Fms-Java版开发实录:18 编写业务模块之前的小插曲 - 恐怖の大修改

Fms-Java版开发实录:18 编写业务模块之前的小插曲 - 恐怖の大修改

添加了印花税的功能

由于现在项目达到了一个里程碑节点,业务以外的内容都编写完毕了,晚上突发奇想,要不要打一个jar包来运行试试。

结果这一打不要紧,运行的时候发现提示Thymeleaf找不到模板,这是为什么呢?

Thymeleaf的路径要注意

赶快查了一遍,发现原因是Thymeleaf的模板路径最前边不能够加/,在引用fragment的时候同样也不能加/,控制器返回的模板路径的前边也不能加/
例如基础模板standardwithnavbar.html里的这行:

<th:block th:replace="~{/shared/navbar::navbar}"/>

这个红色的/就是问题所在,只要删除了就好了。

还有就是控制器,必须返回不带/的视图名称,比如:

return "account/login";

这一改不要紧,几乎所有的控制器连同模板全部需要修改:
fms-thymeleaf

此外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>&nbsp;新增条目</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请求控制器

逻辑就是先校验,再判断是否重复,所以需要在StampServiceStampDao中添加对应的方法。这里要注意的是,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";
            }
        }
    }

印花税的删除

删除功能目前没有写,这是因为印花税是基础的设置之一,如果某个合同关联到了印花税,在删除之前必须确定没有合同使用到那个印花税设置。这个功能如果要写,也要到以后了。

LICENSED UNDER CC BY-NC-SA 4.0
Comment