Публичный аккаунт: MarkerHub (подпишитесь, чтобы узнать больше о ресурсах проекта)
репозиторий кода блога:GitHub.com/маркер-хаб/о…
видео проекта в блоге:воооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооо
Каталог документации по разработке:
(Eblog) 1. Строительство архитектуры проекта, инициализация домашней страницы
(блог) 4. Настройте метку Freemaker, чтобы реализовать заполнение данными главной страницы блога.
(блог) 5. Заполнение классификации блога, логика регистрации входа
(блог) 6. Публикация в блогах коллекций, настройки пользовательского центра
(блог) 7. Асинхронное уведомление о сообщениях, настройка деталей
(Eblog) 8, Разработка поисковой системы блога, выбор фона
(блог) 9. Мгновенная разработка группового чата, записи чата и т. д.
Чтобы просмотреть vueblog проекта разделения клиентской и серверной частей, нажмите здесь:Супер подробно! 4 часа на разработку блог-проекта SpringBoot+vue, разделяющего интерфейс и серверную часть! !
На этот раз давайте улучшим содержимое домашней страницы, например список статей на главной странице, классификацию навигации на домашней странице, список классификации и сведения о статье.
При этом я написал много ошибок в домашнем задании прошлого выпуска, а потом многие из них тайком исправил, причем все относительно подробно.Может не все напишу.Если не знаете где Я изменил, есть два метода:
- 1. Посмотрите на запись коммита git, нажмите на файл, чтобы сравнить его.
- 2. Запустите мой проект и ваш проект, свяжите единую базу данных и оцените, соответствуют ли отображение содержимого и функция страницы.Несоответствие означает, что я тайно исправил некоторые неизвестные ошибки.
1. Наполнение главной страницы контентом
Список страниц
Упомянутая здесь подкачка списка относится к списку содержимого домашней страницы.Вы можете видеть, что содержимое каждой строки содержимого списка фактически совпадает с верхним списком, поэтому мы можем повторно использовать исходный sql. Включение списка конкретных навигационных категорий, на которые мы нажали позже, также является последовательным. Если содержимое одинаковое, мы можем подумать, что, во-первых, мы можем предложить интерфейсный список в качестве шаблона, чтобы все места можно было изменить только один раз, и всеми местами можно было управлять.
Тогда есть два способа справиться с бэкендом:
-
1. Продолжайте использовать наш метод маркировки freemarker
-
2. Используйте контроллер для передачи данных на внешний интерфейс
Мы изучили способ тегов раньше, поэтому на этот раз мы отправляем данные во внешний интерфейс в контроллере.
Сначала посмотрите на контроллер домашней страницы
@RequestMapping({"", "/", "/index"})
public String index () {
IPage results = postService.paging(getPage(), null, null, null, null, "created");
req.setAttribute("pageData", results);
return "index";
}
Приведенный выше postService.paging — это то, что мы писали ранее, но есть еще несколько параметров, которые я тайно изменил. Покажи последнюю версию
@Override
@Cacheable(cacheNames = "cache_post", key = "'page_' + #page.current + '_' + #page.size " +
"+ '_query_' +#userId + '_' + #categoryId + '_' + #level + '_' + #recommend + '_' + #order")
public IPage paging(Page page, Long userId, Long categoryId, Integer level, Boolean recommend, String order) {
if(level == null) level = -1;
QueryWrapper wrapper = new QueryWrapper<Post>()
.eq(userId != null, "user_id", userId)
.eq(categoryId != null && categoryId != 0, "category_id", categoryId)
.gt(level > 0, "level", 0)
.eq(level == 0, "level", 0)
.eq(recommend != null, "recommend", recommend)
.orderByDesc(order);
IPage<PostVo> pageData = postMapper.selectPosts(page, wrapper);
return pageData;
}
По сути, это добавить еще несколько параметров, чтобы справиться с большим количеством сценариев. Возвращаясь к только что упомянутому методу index, я написал в BaseController функцию getPage(), которая обеспечивает сбор и инкапсуляцию данных подкачки, получение информации о подкачке внешнего интерфейса, а затем инкапсулирует ее в объект poge. Затем дайте параметру значение по умолчанию.
public Page getPage() {
int pn = ServletRequestUtils.getIntParameter(req, "pn", 1);
int size = ServletRequestUtils.getIntParameter(req, "size", 10);
Page page = new Page(pn, size);
return page;
}
Затем вы можете увидеть, что в индексе я передал объект pageData во внешний интерфейс, давайте посмотрим на внешний интерфейс.
<ul class="fly-list">
<#list pageData.records as post>
<@listing post></@listing>
</#list>
</ul>
Найдите часть содержимого посередине и напишите ее, как указано выше, потому что я инкапсулирую список записей в макрос (макрос).
Конкретное содержание это
<#macro listing post>
<li>
<a href="${base}/user/${post.authorId}" class="fly-avatar">
<img src="${post.authorAvatar}" alt="${post.authorName}">
</a>
<h2>
<a class="layui-badge">${post.categoryName}</a>
<a href="${base}/post/${post.id}">${post.title}</a>
</h2>
<div class="fly-list-info">
<a href="${base}/user/${post.authorId}" link>
<cite>${post.authorName}</cite>
<i class="layui-badge fly-badge-vip">VIP${post.authorVip}</i>
</a>
<span>${post.created?string('yyyy-MM-dd')}</span>
<span class="fly-list-nums">
<i class="iconfont icon-pinglun1" title="回答"></i> ${post.commentCount}
</span>
</div>
<div class="fly-list-badge">
<#if post.level gt 0><span class="layui-badge layui-bg-black">置顶</span></#if>
<#if post.recommend><span class="layui-badge layui-bg-red">精帖</span></#if>
</div>
</li>
</#macro>
Что касается использования макроса тега freemarker, если вы его не понимаете, обратитесь к Baidu. Кажется, мы говорили об этом раньше, верно? забыл~
<#macro listing post>...</#macro>
Это означает, что термин макроса определен как список, параметр — это сообщение, а содержимое метки — это содержимое макроса. Вы можете использовать метку непосредственно там, где вам нужно вызвать этот макрос, чтобы вы могли видеть, что я только что написал.
Ну, список зациклен, но есть проблема, которая не решена, то есть проблема с пейджингом.В интерфейсе нам нужна навигация с разбивкой на страницы, чтобы дать нам количество страниц, которые нужно щелкнуть. На втором этапе домашнего задания мы использовали другой плагин, на этот раз мы напрямую используем плагин пейджинга от layui, который довольно прост. Поскольку этот номер страницы пейджинга по-прежнему нужен для многих номеров страниц, поэтому я сделал макрос для содержимого пейджинга, а затем ссылаюсь на метод написания пейджинга лайуиWoohoo.lay UI.com/demo/Двенадцатый лунный месяц прополз…
<#--分页模板-->
<#macro page data>
<div id="laypage-main"></div>
<script type="application/javascript">
$(function () {
layui.use(['laypage', 'layer'], function(){
var laypage = layui.laypage
,layer = layui.layer;
//总页数大于页码总数
laypage.render({
elem: 'laypage-main'
,count: ${data.total} //数据总数
,curr: ${data.current}
,limit: ${data.size}
,jump: function(obj, first){
console.log(obj)
//首次不执行
if(!first){
var url = window.location.href;
location.href = "?pn=" + obj.curr;
}
}
});
});
});
</script>
</#macro>
Приведенный выше js относительно прост, просто вызовите laypage.render из layui, чтобы отобразить номер страницы.Мы вызываем код там, где он нам нужен.
<div style="text-align: center">
<@page pageData></@page>
</div>
pageData — это данные, передаваемые контроллером, а эффект рендеринга выглядит следующим образом:
Это идеально, я такой гений, всем это нравится, и цветы расцветают, когда они это видят~
Категория навигации
Далее давайте улучшим навигационную классификационную информацию. Это относительно просто. У нас есть таблица для хранения классификационной информации. Нам нужно только получить список (идентификатор, имя) и не нужно связывать другие таблицы, тогда mybatis plus может мне помочь прямо Готово, мне сервис писать не надо, тогда думаю надо отправить туда данные еще раз, индекс домашней страницы? Навигационная классификация используется везде, поэтому она не подходит В настоящее время это хороший способ определить тег freemarker.
Но здесь нам не нужно использовать метод метки.Я помещаю данные в глобальный контекст приложения Context, поэтому, когда мы инициализируем проект, мы загружаем данные, что похоже на нашу предыдущую инициализацию на этой неделе, поэтому мы сразу начинаем класс, в который добавляется наш код в
хорошо, 2 строки кода, никогда не пишите слишком много, у некоторых людей может быть статус для управления отображением классификации, что может быть условием.
currentCategoryId должен отображать текущую выбранную категорию, по умолчанию 0 (домашняя страница)
Глядя на внешний интерфейс, он должен отображать данные:
<li class="${(0 == currentCategoryId)?string('layui-hide-xs layui-this', '')}">
<a href="/">首页</a>
</li>
<#list categorys as category>
<li class="${(category.id == currentCategoryId)?string('layui-hide-xs layui-this', '')}">
<a href="${base}/category/${category.id}">${category.name}</a>
</li>
</#list>
эмм~, обратите внимание на бинарное написание freemarker, остальные простые~
Детали категории
Нажав на категорию навигации, мы перейдем кhttp://localhost:8080/category/1, содержимое снова похоже на наш список домашней страницы, com.example.controller.PostControllerсередина
@RequestMapping("/category/{id:\\d*}")
public String category(@PathVariable Long id) {
Page page = getPage();
IPage<PostVo> pageData = postService.paging(page, null, id, null, null, "created");
req.setAttribute("pageData", pageData);
req.setAttribute("currentCategoryId", id);
return "post/category";
}
currentCategoryId должен отображать мою текущую выбранную категорию.
- templates/post/category.ftl
<ul class="fly-list">
<#list pageData.records as post>
<@listing post></@listing>
</#list>
</ul>
<!-- <div class="fly-none">没有相关数据</div> -->
<div style="text-align: center">
<@page pageData></@page>
</div>
Подробности блога
Что ж, давайте посмотрим на подробности блога, щелкните страницу для перехода после списка и отобразим такую информацию, как содержимое блога и список комментариев.
- com.example.controller.PostController
@RequestMapping("/post/{id:\\d*}")
public String view(@PathVariable Long id) {
QueryWrapper wrapper = new QueryWrapper<Post>()
.eq(id != null, "p.id", id);
PostVo vo = postService.selectOne(wrapper);
IPage commentPage = commentService.paging(getPage(), null, id, "id");
req.setAttribute("post", vo);
req.setAttribute("pageData", commentPage);
return "post/view";
}
Я написал два вышеуказанных метода обслуживания
- postService.selectOne
Среди них sql метода selectOne на самом деле такой же, как selectPosts исходного сообщения, но возвращаемый — пейджинг, один bean-компонент, а параметр не имеет объекта страницы.
<select id="selectOne" resultType="com.example.vo.PostVo">
select p.*
, c.id as categoryId, c.name as categoryName
, u.id as authorId, u.username as authorName, u.avatar as authorAvatar, u.vip_level as authorVip
from post p
left join user u on p.user_id = u.id
left join category c on p.category_id = c.id
${ew.customSqlSegment}
commentService.paging
Для этого метода я написал комментарий Vo для передачи данных и добавления некоторой связанной информации в vo, такой как имя пользователя и т. д.
<select id="selectComments" resultType="com.example.vo.CommentVo">
select c.*
, u.id as authorId, u.username as authorName, u.avatar as authorAvatar, u.vip_level as authorVip
from comment c
left join user u on c.user_id = u.id
${ew.customSqlSegment}
</select>
Внешний интерфейс прост, просто отображайте данные в цикле списка.
<#list pageData.records as comment>
...
</#list>
<!--分页-->
<div style="text-align: center">
<@page pageData></@page>
</div>
Глядя на наш код конкретно, нет необходимости публиковать его здесь. Что ж, дисплей данных здесь первый~
2. Статус пользователя
Мы завершили отображение данных выше. Чтобы редактировать данные, нам нужно использовать разрешение вошедшего в систему пользователя, поэтому перед редактированием давайте решим проблему аутентификации пользователя. Здесь мы используем структуру shiro для ее завершения.
Что касается модуля входа, давайте сначала прочесаем вашу логику, сначала скопируем страницу зарегистрированной страницы, затем изменим форму шаблона (голова и хвост, боковая панель и т. д.), затем интегрируем фреймворк Широ, напишем интерфейс регистрации входа, вход -> Realm - > Написать логику регистрации входа -> Метка Широ страницы -> Конфигурация распределенного сеанса, затем
Логика входа
- com.example.controller.IndexController
@GetMapping("/login")
public String login() {
return "auth/login";
}
@ResponseBody
@PostMapping("/login")
public Result doLogin(String email, String password) {
if(StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) {
return Result.fail("用户名或密码不能为空!");
}
AuthenticationToken token = new UsernamePasswordToken(email, SecureUtil.md5(password));
try {
//尝试登陆,将会调用realm的认证方法
SecurityUtils.getSubject().login(token);
}catch (AuthenticationException e) {
if (e instanceof UnknownAccountException) {
return Result.fail("用户不存在");
} else if (e instanceof LockedAccountException) {
return Result.fail("用户被禁用");
} else if (e instanceof IncorrectCredentialsException) {
return Result.fail("密码错误");
} else {
return Result.fail("用户认证失败");
}
}
return Result.succ("登录成功", null, "/");
}
@GetMapping("/register")
public String register() {
return "auth/register";
}
@ResponseBody
@PostMapping("/register")
public Result doRegister(User user, String captcha, String repass) {
String kaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(KAPTCHA_SESSION_KEY);
if(!kaptcha.equalsIgnoreCase(captcha)) {
return Result.fail("验证码不正确");
}
if(repass == null || !repass.equals(user.getPassword())) {
return Result.fail("两次输入密码不一致");
}
Result result = userService.register(user);
result.setAction("/login"); // 注册成功之后跳转的页面
return result;
}
@GetMapping("/logout")
public String logout() throws IOException {
SecurityUtils.getSubject().logout();
return "redirect:/";
}
В приведенном выше коде мы сначала написали методы get и post для входа и регистрации соответственно, один из них - перейти к входу в систему, а затем мы отправляем данные формы с помощью асинхронного метода публикации Основная логика входа в систему очень проста, в основном всего одна строка. кода:
SecurityUtils.getSubject().login(token);
根据我们对shiro的理解,login之后会最终委托给realm完成登录逻辑的认证,那么我们先来看看realm的内容(doGetAuthenticationInfo)
@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//注意token.getUsername()是指email!!
AccountProfile profile = userService.login(token.getUsername(), String.valueOf(token.getPassword()));
log.info("---------------->进入认证步骤");
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profile, token.getCredentials(), getName());
return info;
}
}
doGetAuthenticationInfo — это наш метод аутентификации, а authenticationToken — это переданный нам UsernamePasswordToken, который содержит адрес электронной почты и пароль. Затем содержимое userService.login предназначено для проверки правильности учетной записи.Если это недопустимо, будет выдано соответствующее исключение.Если это допустимо, будет возвращен объект пакета AccountProfile.
@Override
public AccountProfile login(String username, String password) {
log.info("------------>进入用户登录判断,获取用户信息步骤");
User user = this.getOne(new QueryWrapper<User>().eq("email", username));
if(user == null) {
throw new UnknownAccountException("账户不存在");
}
if(!user.getPassword().equals(password)) {
throw new IncorrectCredentialsException("密码错误");
}
//更新最后登录时间
user.setLasted(new Date());
this.updateById(user);
AccountProfile profile = new AccountProfile();
BeanUtil.copyProperties(user, profile);
return profile;
}
хорошо, с логикой разобрались, сделаем это на следующей странице, а потом получим логику регистрации.
логика регистрации
Процесс регистрации оформлен в виде плагина проверки кода подтверждения, здесь мы используем капчу генератора кода подтверждения Google.
Давайте сначала интегрируем, сначала импортируем пакет jar
<!--验证码-->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
Затем настройте правила генерации изображения для проверочного кода: (граница, цвет, размер шрифта, длина, высота)
@Configuration
public class WebMvcConfig {
@Bean
public DefaultKaptcha producer () {
Properties propertis = new Properties();
propertis.put("kaptcha.border", "no");
propertis.put("kaptcha.image.height", "38");
propertis.put("kaptcha.image.width", "150");
propertis.put("kaptcha.textproducer.font.color", "black");
propertis.put("kaptcha.textproducer.font.size", "32");
Config config = new Config(propertis);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
Что ж, мы интегрировали плагин, далее мы предоставляем интерфейс доступа для генерации картинок с проверочным кодом, сначала внедряем плагин.
@Autowired
private Producer producer;
然后com.example.controller.IndexController中
@GetMapping("/capthca.jpg")
public void captcha(HttpServletResponse response) throws IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
//生成文字验证码
String text = producer.createText();
//生成图片验证码
BufferedImage image = producer.createImage(text);
//把验证码存到shrio的session中
SecurityUtils.getSubject().getSession().setAttribute(KAPTCHA_SESSION_KEY, text);
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(image, "jpg", outputStream);
}
Итак, войдите в этот интерфейс, чтобы получить поток изображения кода подтверждения на странице:
<img
id
=
"capthca"
src
=
"/capthca.jpg"
>
Затем поток подключается к интерфейсу и серверу, и правильность проверочного кода необходимо проверить на бэкэнде, поэтому при генерации проверочного кода нам нужно сначала сохранить проверочный код в сеансе. , затем зарегистрируйте интерфейс, а затем получите его из сеанса и сравните, правильно это или нет.
@ResponseBody
@PostMapping("/register")
public Result doRegister(User user, String captcha, String repass) {
String kaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(KAPTCHA_SESSION_KEY);
if(!kaptcha.equalsIgnoreCase(captcha)) {
return Result.fail("验证码不正确");
}
...
return result;
}
Итак, первое, что нужно сделать для регистрации интерфейса, это проверить правильность проверочного кода.
Result
result
=
userService
.
register
(
user
);
Давайте посмотрим на логику внутри
@Override
public Result register(User user) {
if(StringUtils.isEmpty(user.getEmail()) || StringUtils.isEmpty(user.getPassword())
|| StringUtils.isEmpty(user.getUsername())) {
return Result.fail("必要字段不能为空");
}
User po = this.getOne(new QueryWrapper<User>().eq("email", user.getEmail()));
if(po != null) {
return Result.fail("邮箱已被注册");
}
String passMd5 = SecureUtil.md5(user.getPassword());
po = new User();
po.setEmail(user.getEmail());
po.setPassword(passMd5);
po.setCreated(new Date());
po.setUsername(user.getUsername());
po.setAvatar("/res/images/avatar/default.png");
po.setPoint(0);
return this.save(po)? Result.succ("") : Result.fail("注册失败");
}
На самом деле это проверка зарегистрирован ли уже пользователь.Если нет,вставьте запись.Я тут только шифрую пароль с помощью md5.Если вам кажется, что шифрование пароля недостаточно строгое,то можете добавить соль или изменить к другим методам шифрования. хорошо, мы закончили логику регистрации бэкенда, давайте взглянем на фронтенд. layui помог нам инкапсулировать логику отправки формы
Следовательно, атрибуты в возвращаемом значении должны иметь действие, статус, сообщение и т. д. Таким образом, класс Result, который мы инкапсулировали ранее, теперь необходимо изменить. Раньше наш Result содержал только код, данные и сообщение, теперь мы добавляем действие и статус.
- com.example.common.lang.Result
@Data
public class Result implements Serializable {
private Integer code;
private Integer status;
private String msg;
private Object data;
private String action;
...
}
Это наш последний возвращенный класс инкапсуляции.Есть также несколько методов инкапсуляции для просмотра конкретного кода. Таким образом, возвращенное значение зарегистрированного метода заканчивается следующим образом:
Result result = userService.register(user);
result.setAction("/login"); // 注册成功之后跳转的页面
return result;
action представляет собой ссылку для перехода после успешной обработки формы.
Как вы можете видеть выше, после того, как я нажал кнопку ОК, что операция прошла успешно, меня перебросило на страницу входа в систему, что и установлено здесь моим действием.
Мы только что завершили логику на бизнес-уровне, теперь давайте посмотрим на сторону страницы. Фоновый интерфейс оригинального макета уже помог нам завершить логику страницы. На самом деле никакой логики нет, после того, как форма формы соответствует полю, мы знаем, что уже есть кнопка отправки для мониторинга всех форм форм в js, которая вызовет следующий метод:
- static/res/mods/index.js
//表单提交
form.on('submit(*)', function(data){
var action = $(data.form).attr('action'), button = $(data.elem);
fly.json(action, data.field, function(res){
var end = function(){
if(res.action){
location.href = res.action;
} else {
fly.form[action||button.attr('key')](data.field, data.form);
}
};
if(res.status == 0){
button.attr('alert') ? layer.alert(res.msg, {
icon: 1,
time: 10*1000,
end: end
}) : end();
};
});
return false;
});
Итак, в странице регистрации нам не нужно писать какой js, вы можете проверить код события клика на картинку, потому что иногда вы не можете увидеть клик для другого.
- templates/auth/register.ftl
<script>
$(function () {
$("#capthca").click(function () {
this.src="/capthca.jpg";
});
});
</script>
Нам не нужно писать какие-либо js на странице входа. Возьми! Выше приведена наша логика регистрации.
теги страницы Широ
Затем мы используем некоторые теги shiro на внешнем интерфейсе, чтобы мы могли управлять разрешениями кнопок, статусом входа пользователя, информацией о пользователе и т. д. на странице. Поскольку на нашей странице используется freemarker, мы используем jar-пакет freemarker-shiro.
<dependency>
<groupId>net.mingsoft</groupId>
<artifactId>shiro-freemarker-tags</artifactId>
<version>0.1</version>
</dependency>
На втором этапе вам нужно внедрить тег shiro в конфигурацию тега freemarker:
- com.example.config.FreemarkerConfig
Третий шаг, мы отображаем информацию после входа пользователя в правый верхний угол страницы.
смутно помним, что содержимое нашего шапки помещается в
- templates/inc/header.ftl
Итак, как вы используете теги Широ? Для конкретного использования, пожалуйста, ознакомьтесь с этой статьей.
<@shiro.guest>
<@shiro.user>
*<@shiro.principal property="username" */>
Итак, после изучения тега широ мы можем его использовать.
<ul class="layui-nav fly-nav-user">
<@shiro.guest>
<!-- 未登入的状态 -->
<li class="layui-nav-item">
<a class="iconfont icon-touxiang layui-hide-xs" href="/login"></a>
</li>
<li class="layui-nav-item">
<a href="/login">登入</a>
</li>
<li class="layui-nav-item">
<a href="/register">注册</a>
</li>
</@shiro.guest>
<@shiro.user>
<!-- 登入后的状态 -->
<li class="layui-nav-item">
<a class="fly-nav-avatar" href="javascript:;">
<cite class="layui-hide-xs"><@shiro.principal property="username" /></cite>
<i class="iconfont icon-renzheng layui-hide-xs" title="认证信息:layui 作者"></i>
<i class="layui-badge fly-badge-vip layui-hide-xs">VIP<@shiro.principal property="vipLevel" /></i>
<img src="<@shiro.principal property="avatar" />">
</a>
<dl class="layui-nav-child">
<dd><a href="user/set.html"><i class="layui-icon"></i>基本设置</a></dd>
<dd><a href="user/message.html"><i class="iconfont icon-tongzhi" style="top: 4px;"></i>我的消息</a></dd>
<dd><a href="user/home.html"><i class="layui-icon" style="margin-left: 2px; font-size: 22px;"></i>我的主页</a></dd>
<hr style="margin: 5px 0;">
<dd><a href="/logout" style="text-align: center;">退出</a></dd>
</dl>
</li>
</@shiro.user>
</ul>
Вышеупомянутое предназначено для определения того, вошел ли пользователь в систему с помощью двух тегов <@shiro.guest> и <@shiro.user>. Перед входом в систему мы видим кнопку регистрации входа, а после входа мы видим имя пользователя, аватар и т. д. ~@shiro.user>@shiro.guest>
Хорошо, у нас есть тег Широ выше~
Сегодняшняя домашняя работа здесь в первую очередь, все сначала выполняют функцию входа и регистрации.