由于现在项目达到了一个里程碑节点,业务以外的内容都编写完毕了,晚上突发奇想,要不要打一个jar包来运行试试。
结果这一打不要紧,运行的时候发现提示Thymeleaf
找不到模板,这是为什么呢?
Thymeleaf
的路径要注意
赶快查了一遍,发现原因是Thymeleaf
的模板路径最前边不能够加/
,在引用fragment
的时候同样也不能加/
,控制器返回的模板路径的前边也不能加/
。
例如基础模板standardwithnavbar.html
里的这行:
<th:block th:replace="~{/shared/navbar::navbar}"/>
这个红色的/
就是问题所在,只要删除了就好了。
还有就是控制器,必须返回不带/
的视图名称,比如:
return "account/login";
这一改不要紧,几乎所有的控制器连同模板全部需要修改:
此外java 11
在运行项目的时候会提示非法的反射操作,这是因为JDK 11
对于反射的限制更加严格了,所以要加上--illegal-acess=deny
参数来启动项目,如下:
java --illegal-access=deny -jar fms.jar
经过一番折腾,项目终于可以成功的运行了。以后部署也方便很多了。
管理员目前还差一个功能,就是印花税功能。虽然简单,但印花税也需要一个完整的增删改查,和用户功能一样,所以后边来写掉。从编写管理员功能开始,我新开了一个分支adminFunction
,现在可以合并分支啦,合并之后,再开一个分支stamp
用来编写印花税功能。
天气热了,空气好了,是时候考虑恢复跑步了,不再靠运动量比较低的打拳来锻炼啦。
印花税
印花税这个东西虽然说不一定用得上,不过设置在系统里也比没有强。合同更是可以通过关联的印花税来进行计算。
PO
类与初始写入数据库的数据
PO
类如下:
@Entity
@Table(name = "stamp")
public class Stamp implements Serializable {
private static final long serialVersionUID = 6L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@NotEmpty(message = "印花税合同类型不能为空")
@Column(name = "type", nullable = false, unique = true)
@Length(max = 20, message = "长度不能超过20个字符")
private String type;
@NotNull(message = "税率不能为空")
@Digits(integer = 0, fraction = 5)
@DecimalMax(value = "0.00100", message = "税率超出印花税上限")
@DecimalMin(value = "0.00000", message = "税率低于零")
private BigDecimal rate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "create_time", updatable = false)
@CreationTimestamp
private Date createTime;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "update_time")
@UpdateTimestamp
protected Date updateTime;
public String absoluteUrl() {
return "/admin/stamp/" + this.id;
}
public void setId(Integer id) {
this.id = id;
}
}
有了@Digits
注解,Hibernate
在建表的时候会自动将字段设置为NUMERIC(5,5)
,符合我们的要求。
这里还有一个小知识,经过发现,Entity
类的Id
是不能够通过反射获取的,必须在类中编写getter
方法才行。
对应的初始化语句如下:
-- 写入印花税设置
INSERT INTO stamp(create_time, type, rate, update_time)
VALUES (CURRENT_TIMESTAMP, '购销合同', 0.00030, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp(create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '加工承揽合同', 0.00050, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '建设工程勘察设计合同', 0.00050, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES (CURRENT_TIMESTAMP, '建筑安装工程承包合同', 0.00030, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '财产租赁合同', 0.00100, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp(create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '货物运输合同', 0.00050, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '仓储保管合同', 0.00100, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES (CURRENT_TIMESTAMP, '借款合同', 0.00005, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '财产保险合同', 0.00003, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '技术合同', 0.00030, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES ( CURRENT_TIMESTAMP, '产权转移书据', 0.00050, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
INSERT INTO stamp( create_time, type, rate, update_time)
VALUES (CURRENT_TIMESTAMP, '无需缴纳', 0.00000, CURRENT_TIMESTAMP)
ON CONFLICT (type) DO NOTHING;
注意这里不要插入id
,让Hibernate
自己插入,否则会导致数据库的SEQUENCE
生成的数据出现问题。
展示页面编写
用一个迭代就可以展示了,类似于用户编辑,也使用id
拼接的路径:
<main class="container">
<h2 class="pb-2 py-3">印花税设置 <span class="text-secondary h6">印花税条目: [[${count}]]</span></h2>
<a class="btn btn-primary btn-sm" href="/admin/stamp/add" th:href="@{/admin/stamp/add}"><i class="fas fa-plus"></i> 新增条目</a>
<div class="row">
<div class="col-12 col-md-6 col-lg-4">
<table class="table table-hover table-sm">
<thead>
<tr>
<th>类型</th>
<th>税率</th>
</tr>
</thead>
<tbody>
<tr th:each="stamp:${stamps}">
<td><a th:href="${stamp.absoluteUrl()}">[[${stamp.type}]]</a></td>
<td th:text="${stamp.rate}"></td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
增删改功能编写
印花税的本质实际上和后边将要编写的所有业务都一样,需要完整的增删改查功能。之前的列表页已经做上了添加和详情页=编辑页的链接,现在就是要加上编辑和新增功能。
至于删除功能现在是不好做滴,为什么请看最后的分析。。
详情页及GET
控制器
详情页stampEdit.html
的编写也很简单,就一个表单:
<main class="container">
<h2 class="pb-2 py-3">修改印花税 <span class="text-primary h4">[[${stamp.type}]]</span></h2>
<div class="row">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<form action="/admin/stamp" th:action="@{/admin/stamp}" th:object="${stamp}" method="post">
<input type="hidden" name="id" th:value="*{id}">
<div class="input-group mb-3">
<span class="input-group-text" id="type">印花税类型</span>
<input th:classappend="${#fields.hasErrors('type')}? 'is-invalid'" type="text"
class="form-control" th:field="*{type}"
aria-label="username" aria-describedby="typeError">
<div id="typeError" th:if="${#fields.hasErrors('type')}"
th:errors="*{type}"
class="invalid-feedback"></div>
</div>
<div class="input-group mb-1">
<span class="input-group-text" id="rate">印花税税率</span>
<input th:classappend="${#fields.hasErrors('rate')}? 'is-invalid'"
type="number" min="0" step="0.00001" class="form-control" th:field="*{rate}"
aria-label="host"
aria-describedby="rateError">
<div id="rateError" th:if="${#fields.hasErrors('rate')}"
th:errors="*{rate}"
class="invalid-feedback"></div>
</div>
<p class="text-secondary">请填写小数形式的税率</p>
<div class="mt-3">
<button type="submit" class="btn btn-primary">提交</button>
<a th:href="@{/admin/stamp}" href="/admin/stamp" class="btn btn-secondary">返回</a>
</div>
</form>
</div>
<div class="col"></div>
</div>
</main>
对应的控制器如下:
//印花税详情页面
@GetMapping("/stamp/{id}")
public String stampDetail(@PathVariable(name = "id") int id, Model model) {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("isAdmin", user.hasRole("ADMIN"));
model.addAttribute("stamp", stampService.getStampById(id));
return "admin/stampEdit";
}
这个也是老套路了,不再赘述。
接受修改的POST
控制器
上边页面里的表单对应的POST
地址依然是/admin/stamp
,编写的控制器如下:
//修改印花税页面
@PostMapping("/stamp")
public String editStamp(@Valid @ModelAttribute("stamp") Stamp stamp, BindingResult rs, Model model) {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("isAdmin", user.hasRole("ADMIN"));
if (rs.hasErrors()) {
model.addAttribute("stamp", stamp);
return "admin/stampEdit";
}
Stamp stampFromDatabase = stampService.getStampById(stamp.getId());
stampFromDatabase.setRate(stamp.getRate());
stampFromDatabase.setType(stamp.getType());
stampService.saveStamp(stampFromDatabase);
model.addAttribute("stamps", stampService.getAllStamps());
model.addAttribute("count", stampService.getCount());
model.addAttribute("updated", true);
return "admin/stamp";
}
这里会使用埋入表单的id取出来数据然后在上边修改和保存,没有直接去保存对象,还是稳妥起见。
新增印花税页面及控制器
新增印花税页面和修改的表单几乎是一样的,就是要添加一个提示是不是类型名称重复:
<main class="container">
<h2 class="pb-2 py-3">新增印花税</h2>
<div class="row">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<form action="/admin/stamp/add" th:action="@{/admin/stamp/add}" th:object="${stamp}" method="post">
<div th:if="${existed}" class="alert alert-danger mt-2" role="alert">
该印花税类型已经存在
</div>
<div class="input-group mb-1">
<span class="input-group-text" id="type">印花税类型</span>
<input th:classappend="${#fields.hasErrors('type')}? 'is-invalid'" type="text"
class="form-control" th:field="*{type}"
aria-label="username" aria-describedby="typeError">
<div id="typeError" th:if="${#fields.hasErrors('type')}"
th:errors="*{type}"
class="invalid-feedback"></div>
</div>
<p class="text-secondary">类型名称不要与已存在类型重复</p>
<div class="input-group mb-1">
<span class="input-group-text" id="rate">印花税税率</span>
<input th:classappend="${#fields.hasErrors('rate')}? 'is-invalid'"
type="number" min="0" step="0.00001" class="form-control" th:field="*{rate}"
aria-label="host"
aria-describedby="rateError">
<div id="rateError" th:if="${#fields.hasErrors('rate')}"
th:errors="*{rate}"
class="invalid-feedback"></div>
</div>
<p class="text-secondary">请填写小数形式的税率</p>
<div class="mt-3">
<button type="submit" class="btn btn-primary">提交</button>
<a th:href="@{/admin/stamp}" href="/admin/stamp" class="btn btn-secondary">返回</a>
</div>
</form>
</div>
<div class="col"></div>
</div>
</main>
GET
控制器比较简单:
//新增印花税页面
@GetMapping("/stamp/add")
public String stampAddPage(@ModelAttribute("stamp") Stamp stamp, Model model) {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("isAdmin", user.hasRole("ADMIN"));
return "admin/stampAdd";
}
POST
请求控制器
逻辑就是先校验,再判断是否重复,所以需要在StampService
和StampDao
中添加对应的方法。这里要注意的是,StampDao
我继承了JpaRepository
而不是CrudRepository
,这个接口有findAll
方法可以排序,比较方便,StampService
添加的几个方法如下:
@Transactional
public Iterable<Stamp> getAllStamps() {
return stampDao.findAll(Sort.by(Sort.Direction.ASC, "id"));
}
@Transactional
public long getCount() {
return stampDao.count();
}
@Transactional
public Stamp getStampById(int id) {
return stampDao.findById(id).orElseThrow(() -> new RuntimeException("找不到Stamp对象 id=" + id));
}
这里新学到的套路就是排序以及Optional
对象的.orElseThrow
方法。
控制器的代码如下:
@PostMapping("/stamp/add")
public String addNewStamp(@Valid @ModelAttribute("stamp") Stamp stamp, BindingResult rs, Model model) {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("isAdmin", user.hasRole("ADMIN"));
if (rs.hasErrors()) {
return "admin/stampAdd";
} else {
//检测如果重复
if (stampService.existByType(stamp.getType())) {
model.addAttribute("existed", true);
return "admin/stampAdd";
} else {
stampService.saveStamp(stamp);
return "redirect:/admin/stamp";
}
}
}
印花税的删除
删除功能目前没有写,这是因为印花税是基础的设置之一,如果某个合同关联到了印花税,在删除之前必须确定没有合同使用到那个印花税设置。这个功能如果要写,也要到以后了。