(блог) 8. Разработка поисковой системы для блогов, выбор фона

Spring Boot

Публичный аккаунт: MarkerHub (подпишитесь, чтобы узнать больше о ресурсах проекта)

репозиторий кода блога:GitHub.com/маркер-хаб/о…

видео проекта в блоге:воооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооо


Каталог документации по разработке:

(блог) 1. Построение архитектуры проекта, инициализация домашней страницы

(блог) 2. Интегрируйте Redis и элегантную обработку исключений проекта и инкапсуляцию возвращаемых результатов.

(блог) 3. Используйте упорядоченную коллекцию Redis zset для реализации горячо обсуждаемой функции недели

(блог) 4. Настройте метку Freemaker, чтобы реализовать заполнение данными главной страницы блога.

(блог) 5. Заполнение классификации блога, логика регистрации входа

(блог) 6. Публикация в блогах коллекций, настройки пользовательского центра

(блог) 7. Асинхронное уведомление о сообщениях, настройка деталей

(блог) 8. Разработка поисковой системы для блогов, выбор фона

(блог) 9. Мгновенная разработка группового чата, записи чата и т. д.


Чтобы просмотреть vueblog проекта разделения клиентской и серверной частей, нажмите здесь:Супер подробно! 4 часа на разработку блог-проекта SpringBoot+vue, разделяющего интерфейс и серверную часть! !


Функция поиска

Первоначально я хотел разделить его на весенний облачный проект, но бизнес проекта блога действительно мал, нет необходимости его разделять, и мы уже разделили проект на второй фазе проекта, поэтому студенты, которые хотите изучить Spring Cloud, можете пойти посмотреть. Глядя на второй этап работы, мы будем напрямую делать проект Springboot на этом этапе без разделения модулей.

Функция поиска - ES

В сочетании с тем, что мы узнали, мы изучали поисковые системы ранее, lucene и elasticsearch, lucene больше подходит для одиночных проектов, а не для распределенных.

Для этого поиска мы используем es, а RabbitMq используем для синхронизации содержимого между es и базой данных. Давайте реализуем эти функции одну за другой.

Во-первых, давайте проанализируем функцию, которую мы хотим разработать.

  • Функция поиска

  • es инициализация данных

  • Асинхронная синхронизация между es и базой данных

Есть много способов интегрировать elasticsearch,

  • Сравните собственный клиент TransportClient

  • ElasticsearchTemplate предоставлен весной

  • ElasticsearchRepository, предоставленный spring jpa

Там, где elasticsearchRepository должен быть минимальным количеством разработанного способа использования шаблона или клиентского подхода для транспортировки может быть более гибким.

Мы узнали перед Spring data jpa, как в соответствии с соглашением об именах вы можете проверить, как библиотека особенно удобна при поиске в одной таблице.

В этой разработке мы используем ElasticsearchRepository.Конечно, после введения этого пакета вы также можете использовать ElasticsearchTemplate для разработки. Spring автоматически введет и сгенерирует для вас.

Версия es, которой мы следовали ранее, была elasticsearch-6.4.3.tar.gz. Поэтому мы решили использовать соответствующую версию импортированного пакета.

Найдите соответствующую версию пакета в репозитории maven и обнаружите, что предыдущие версии в основном соответствуют требованиям. Здесь мы используем последнюю версию 2.1.1.RELEASE.

В дополнение к es нам также нужно ввести такие пакеты, как feign и rabbitmq, которые нам нужно будет использовать позже. Здесь я приведу унифицированный первый.

<!-- es -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>
<!--整合rabbitmq-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>1.1.0</version>
</dependency>

В нашем курсе была представлена ​​установка es и mq. Студенты могут ознакомиться с ней или прочитать дополнительные документы нашего сообщества, чтобы завершить развертывание.

бизнес-анализ

Давайте проанализируем это еще раз, потому что мы решили использовать ElasticsearchRepository для доступа к нашему elasticsearch, поэтому согласно этой идее нам нужно подготовить модель и репозиторий, который является основой для доступа к носителю данных, а создание нового репозитория — это очень просто, потому что это весенние данные jpa, поэтому вы можете напрямую наследовать ElasticsearchRepository:

  • com.example.search.repository.PostRepository
@Repository
public interface PostRepository extends ElasticsearchRepository<PostDocument, Long> {
}

Каково содержание модели? Давайте взглянем на внешний список поиска, мы можем хранить любые данные, которые нужны списку. Мы используем поисковую систему, поэтому лучше всего, чтобы результаты поиска не проходили через запрос нашей базы данных, чтобы мы могли напрямую возвращать результаты поиска на внешний дисплей. Тем самым увеличивая скорость поиска. Например, мы смотрим на отображение списка данных. Здесь мы видим, что требуется название, имя автора, идентификатор автора, время создания, количество прочтений и т. д.

Создайте новый класс PostDocment в пакете модели. По сути, это в основном то же самое, что и наше постВо.

@Document(indexName = "post", type = "post")
@Data
public class PostDocument implements Serializable {
    @Id
    private Long id;
    // 中文分词器 -> https://github.com/medcl/elasticsearch-analysis-ik
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String title;
//    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
//    private String content;
    private Long authorId;
    @Field(type = FieldType.Keyword)
    private String authorName;
    private String authorVip;
    private String authorAvatar;
    private Long categoryId;
    @Field(type = FieldType.Keyword)
    private String categoryName;
    private Boolean recommend;
    private Integer level;
    @Field(type = FieldType.Text)
    private String tags;
    private Integer commentCount;
    private Integer viewCount;
    @Field(type = FieldType.Date)
    private Date created;
}

Здесь я отсортировал необходимые поля. Затем вам нужно использовать аннотацию jpa. FieldType.Text указывает, что это текст и нужно пройти сегментацию слов (здесь мы не будем говорить о сегментации слов, и мы поговорим об этом позже). FieldType.Keyword должен точно совпадать.

Здесь я использую текстовый поиск по заголовку и введению, а автор и категория должны точно совпадать для поиска. Analyzer="ikmaxword" о токенизаторе, о котором мы поговорим позже.

С этой сущностью es автоматически поможет нам создать новую структуру индекса данных. Мы можем использовать PostRepository для добавления, удаления, изменения и проверки наших данных es.

Далее создаем новый метод поиска, т.к. требуется пейджинг, нужны параметры страницы и размера. Тогда есть параметр ключевого слова. Это запрос ключевого слова и возврат результатов в разбивке на страницы.

код показывает, как показано ниже:

  • com.example.controller.IndexController
@RequestMapping("/search")
public String search(@RequestParam(defaultValue = "1") Integer current,
                     @RequestParam(defaultValue = "10")Integer size,
                     String q) {
    Pageable pageable = PageRequest.of(current - 1, size);
    Page<PostDocument> documents = searchService.query(pageable, q);
    IPage pageData = new com.baomidou.mybatisplus.extension.plugins.pagination.Page
            (current, size, documents.getTotalElements());
    pageData.setRecords(documents.getContent());
    req.setAttribute("pageData", pageData);
    req.setAttribute("q", q);
    return "search";
}

Во-первых, мы записываем инкапсуляцию подкачки Pageable для данных jpa, и результирующий объект Page также является jpa, но мы делаем слой преобразования, потому что возвращаемый параметр — mybatis plus. Наконец, получите pageData. Основным методом здесь является метод запроса searchService.query.

Создайте новый интерфейс SearchService и класс реализации.Запрос метода запроса на самом деле очень прост, потому что здесь у нас есть только один запрос по ключевому слову и не задействовано много других сложных запросов, поэтому мы просто сначала реализуем его, а затем мы можем добавить веса , сегментация слов и другие вопросы позже.

Логика поиска заключается в том, чтобы ключевые слова соответствовали полям, которые мне нужно запросить, и пока одно из полей совпадает, мы попадаем. Сопоставление нескольких полей мы можем использовать для построения MultiMatchQueryBuilder. Что касается имени поля, я написал IndexKey. Поскольку поиск ищет не только название, но также необходимо искать информацию, такую ​​​​как имя автора, название категории и т. Д., Используйте или связать, чтобы получить окончательный результат

  • com.example.search.common.IndexKey
/**
 * 索引名称
 */
public class IndexKey {
    public static final String POST_TITLE = "title";
    public static final String POST_DESCRIPTION = "content";
    public static final String POST_AUTHOR = "authorName";
    public static final String POST_CATEGORY = "categoryName";
    public static final String POST_TAGS = "tags";
}

Это поля, которые мне нужно найти. После создания соответствия нескольких полей мы используем NativeSearchQueryBuilder для интеграции и разбивки на страницы, чтобы получить результат SearchQuery, который затем можно использовать.postRepository.search(searchQuery); чтобы получить нужные нам результаты разбиения по страницам, не правда ли, это очень просто. Что касается сложного синтаксиса запроса данных Spring jpa, давайте вернемся и посмотрим на содержание нашего курса jpa. Ознакомьтесь с:

  • com.example.service.impl.SearchServiceImpl
@Override
public Page<PostDocument> query(Pageable pageable, String keyword) {
    //多个字段匹配,只要满足一个即可返回结果
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keyword,
            IndexKey.POST_TITLE,
            IndexKey.POST_DESCRIPTION,
            IndexKey.POST_AUTHOR,
            IndexKey.POST_CATEGORY,
            IndexKey.POST_TAGS
    );
    SearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(multiMatchQueryBuilder)
            .withPageable(pageable)
            .build();
    Page<PostDocument> page = postRepository.search(searchQuery);
    log.info("查询 - {} - 的得到结果如下-------------> {}个查询结果,一共{}页",
            keyword, page.getTotalElements(), page.getTotalPages());
    return page;
}

multiMatchQuery поддерживает сопоставление нескольких полей. Таким образом, когда мы запрашиваем ключевые слова, мы возвращаемся назад и запрашиваем эти поля: заголовок, автор, категория и т. д. Последним шагом является создание SearchQuery и использование интерфейса запроса репозитория. Таким образом, мы можем реализовать функцию запроса es, хотя данных пока нет. Затем настроим переднюю часть и найдем js передней части

Сегментация китайских слов

Мы только что создали функцию поиска, но для сегментации поисковых слов в es используется стандартное устройство сегментации слов по умолчанию. Мы все знаем, что сегментация слов является очень важной частью es. Является ли поиск интеллектуальным или нет, зависит от того, является ли сегментация слов хорошо или нет Если эффект сегментации слов хороший, результаты поиска получаются более точными.

Теперь давайте установим китайский токенизатор IK для es. Я нашел хороший токенизатор ik на github

Используйте способ Word очень прост, прежде чем установить слово, давайте тестируем эффект предыдущего слова не установлен, а эффект слова после слова.

Используйте почтальона для проверки. Сначала протестируйте стандартный токенизатор по умолчанию.

http://47.106.38.101:9200/post/_analyze
Headers  Content-Type: application/json
Method POST
Body
{
   "text":"美国留给伊拉克的是个烂摊子吗",
}

Результаты теста следующие:

Видно, что стандартный токенизатор по умолчанию не распознает наш китайский, а просто разделяет каждый символ. Такая сегментация слов может внести много неточностей в результаты поиска.

Затем давайте установим только что упомянутый токенизатор IK. Согласно инструкции ReadMe. Есть два метода установки, здесь мы используем второй: опциональный 2 - использовать для установки elasticsearch-plugin (поддерживается с версии v5.5.1):

.
/bin/
elasticsearch
-
plugin install https
:
//github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip

Вышеупомянутое предназначено для использования команды elasticsearch-plugin для установки подключаемого модуля, а также дает нам ПРИМЕЧАНИЕ, чтобы мы обратили внимание на замену номера версии. Поскольку версия es, которую я установил,6.4.3, поэтому сразу замените два вышеуказанных 6.3.0. Затем напрямую войдите в каталог установки es и успешно выполните установку.

.
/bin/
elasticsearch
-
plugin install https
:
//github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.4.3/elasticsearch-analysis-ik-6.4.3.zip

После успешной установки перезапустите es, завершите процесс напрямую, а затем перезапустите. После перезапуска вы можете протестировать его в соответствии с данной командой. Вот продолжаю тест только сейчас. Добавляем анализатор в body json и тестируем:

{
   "text":"美国留给伊拉克的是个烂摊子吗",
   "analyzer": "ik_smart"
}

Результаты теста:

Видно, что результаты сегментации существенно различаются. Причастие IK дает дваiksmart** **, **ик***макс_слово. **Разница между ними

  • ik_smart* * разобьёт текст на самые мелкие

  • *ik_max_word * сделает самое грубое разделение

Результаты вы можете наблюдать сами.blog.CSDN.net/WeChat_4406…

Ниже приведен пример почтальона, который я использовал для тестирования. Вы можете импортировать его и протестировать самостоятельно:

elasticsearch-test.postman_collection.json

После успешной установки мы можем добавить наш токенизатор в наш код. Изменения кода минимальны, нам просто нужно добавить анализатор атрибутов аннотации и searchAnalyzer в поля сущности PostDocment. Analyzer — это сегментация слов при сохранении, здесь я использую **ikmaxword **, чтобы можно было искать больше слов. searchAnalyzer представляет сегментацию слов при поиске, я использую **ik_smart, ** только для выбора ключевых слов для поиска. Здесь вы можете просмотреть результаты поиска для настройки.

Удалите предыдущий индекс, затем перезапустите проект hw-search, проверьте информацию об индексе, вы можете видеть, что описание и заголовок уже имеют токенизатор (elasticsearch-head).

Выше мы завершили эффект сегментации китайских слов.

начальная синхронизация

Мы выполнили функцию запроса поисковой системы es, но данных еще нет.Если мы инициализируем данные, мы можем запрашивать базу данных пакетами и вставлять их в es.Более распространенный и простой способ:

Ставим эту операцию синхронизации данных в фоновое управление, разрешение на работу есть только у суперадминистратора, поэтому создаем новый AdminController, затем начинаем с /admin, а потом в широ нам нужно перехватить ссылку, начинающуюся с admin, для чего требуется разрешения роли администратора имеют разрешение на работу.

  • com.example.config.ShiroConfig
hashMap
.
put
(
"/admin/**"
,
 
"auth, roles[admin]"
);

Затем смотрим на контроллер

  • com.example.controller.AdminController
@Controller
@RequestMapping("/admin")
public class AdminController extends BaseController {
    @Autowired
    SearchService searchService;
    @ResponseBody
    @PostMapping("/initEsData")
    public Result initEsData() {
        int total = 0;
        int size = 10000;
        Page page = new Page<>();
        for(int i = 1; i < 1000; i ++) {
            page.setCurrent(i);
            page.setSize(size);
            IPage<PostVo> paging = postService.paging(page, null, null, null, null, "created");
            int num = searchService.initEsIndex(paging.getRecords());
            total += num;
            if(num < size) {
                break;
            }
        }
        return Result.succ("ES索引库初始化成功!共" + total + "条记录", null);
    }
}

Приведенная выше логика на самом деле очень проста: запрашивать данные пакетами, а затем сохранять их в es. Когда количество запросов меньше, чем количество каждой страницы, это означает, что это последняя страница. , перерыв заканчивается. searchService.initEsIndex — более критическая логика, по сути, это сопоставление PostVo с PostDocment, а затем вы можете использовать репозиторий для его сохранения.

  • com.example.service.impl.SearchServiceImpl
@Override
public int initEsIndex(List<PostVo> datas) {
    if(datas == null || datas.isEmpty()) return 0;
    List<PostDocument> docs = new ArrayList<>();
    for(PostVo vo : datas) {
        PostDocument doc = modelMapper.map(vo, PostDocument.class);
        docs.add(doc);
    }
    //批量保存
    postRepository.saveAll(docs);
    return docs.size();
}

Затем теперь мы получаем ссылку /admin/initEsData, где эта кнопка инициируется, потому что у нас сейчас нет фона, поэтому мы поместили эту операцию администратора в основные настройки пользовательского центра и добавили новую вкладку ( admin center), в нем есть кнопка для синхронизации ES, нажатие на кнопку инициирует отправку формы. 

Конкретный код:

  • templates/center/setting.ftl
<@shiro.hasRole >
<div class="layui-form layui-form-pane layui-tab-item">
    <form method="post" action="${base}/admin/initEsData">
        <button class="layui-btn" key="set-mine" lay-filter="*" lay-submit alert="true">同步ES</button>
    </form>
</div>
</@shiro.hasRole>

Обратите внимание, что эта кнопка на самом деле является кнопкой отправки формы, поэтому нам больше не нужно писать js, потому что уже есть глобальная форма js.

Изменить синхронизацию

Далее мы выполним некоторую синхронизацию изменения данных.Когда мы добавляем, изменяем или удаляем данные статьи, es может изменять их синхронно.

Здесь мы используем mq.При изменении передачи данных сообщение отправляется в MQ, а затем потребитель mq принимает сообщение и синхронизирует последнее состояние сообщения с ES.

Первоклассная настройка mq. Создайте новый класс RabbitMqConfig в пакете конфигурации. Давайте сначала рассмотрим содержимое очереди сообщений.В RabbitMQ существует несколько типов сообщений, отправляемых и получаемых.

  • Прямой тип (режим маршрута)

  • Тип разветвления (модель публикации-подписки)

  • Тип темы (подстановочный знак)

Здесь мы можем напрямую использовать прямой режим. Это будет включать очередь Queue, обмен Exchange и ключ маршрутизации RouteKey (BindingKey).

Издатель отправляет сообщение на биржу, помещает сообщение в очередь через ключ маршрутизации биржи и очереди и сохраняет его, а затем потребитель подписывается на очередь для обработки сообщения.

Итак, в RabbitMqConfig нам нужно объявить очередь, обмен, ключ маршрута. и связан.

  • com.example.config.RabbitMqConfig
@Configuration
public class RabbitMqConfig {
    // 队列名称
    public final static String ES_QUEUE = "es_queue";
    public final static String ES_EXCHANGE = "es_exchange";
    public final static String ES_BIND_KEY = "es_index_message";
    /**
     * 声明队列
     * @return
     */
    @Bean
    public Queue exQueue() {
        return new Queue(ES_QUEUE);
    }
    /**
     * 声明交换机
     * @return
     */
    @Bean
    DirectExchange exchange() {
        return new DirectExchange(ES_EXCHANGE);
    }
    /**
     * 绑定交换机和队列
     * @param exQueue
     * @param exchange
     * @return
     */
    @Bean
    Binding bindingExchangeMessage(Queue exQueue, DirectExchange exchange) {
        return BindingBuilder.bind(exQueue).to(exchange).with(ES_BIND_KEY);
    }
}

Для сообщений между потребителями и производителями нам нужен общий тип контракта. Здесь мне нужно создать новый шаблон сообщения. Когда производитель публикует сообщение, ему нужно только заполнить шаблон сообщения, а затем отправить его, и потребитель может обработать сообщение в соответствии с шаблоном сообщения. Содержание шаблона сообщения необходимо учитывать, и в первую очередь необходим тип, потому что это синхронизация между данными ES и Database (например, вновь опубликована, обновленная, удаляет статью). Разные типы требуют различных процедур. Тогда идентификатор статьи требуется. Тогда, если ES не может обработать данные, нам нужно повторить попытку, а количество повторных попыток ограничено, поэтому здесь мы определяем поле для количества повторных попыток.

код показывает, как показано ниже:

  • com.example.search.common.PostMqIndexMessage
/**
 * 用于服务之间消息通讯模板
 */
@Data
public class PostMqIndexMessage {
    public static final String CREATE = "create";
    public static final String UPDATE = "update";
    public static final String REMOVE = "remove";
    public static final int MAX_RETRY = 3;
    private Long postId;
    private String type;
    private int retry = 0;
    public PostMqIndexMessage() {
    }
    public PostMqIndexMessage(Long postId, String type) {
        this.postId = postId;
        this.type = type;
    }
    public PostMqIndexMessage(Long postId, String type, int retry) {
        this.postId = postId;
        this.type = type;
        this.retry = retry;
    }
}

Поскольку сообщения отправляются через сериализованные данные json, мы сначала принимаем содержимое сообщения с типом String, а затем преобразуем содержимое с помощью шаблона сообщения. Вы можете использовать инструмент ObjectMapper. Затем следующим шагом будет обработка сообщения в соответствии с типом сообщения.

  • com.example.mq.HandlerMessage
/**
 * 监听异步消息队列
 * 更新搜索内容
 */
@Slf4j
@Component
@RabbitListener(queues = RabbitMqConfig.ES_QUEUE)
public class HandlerMessage {
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    SearchService searchService;
    @RabbitHandler
    public void handler(String content) {
        try {
            PostMqIndexMessage message = objectMapper.readValue(content, PostMqIndexMessage.class);
            switch (message.getType()) {
                case PostMqIndexMessage.CREATE:
                case PostMqIndexMessage.UPDATE:
                    searchService.createOrUpdateIndex(message);
                    break;
                case PostMqIndexMessage.REMOVE:
                    searchService.removeIndex(message);
                    break;
                default:
                    log.warn("没有找到对应的消息类型,请注意!!!, --->  {}", content);
                    break;
            }
        } catch (IOException e) {
            log.error("这是内容----> {}", content);
            log.error("处理HandlerMessage失败 --> ", e);
        }
    }
}

Далее мы перейдем к SearchService, чтобы написать соответствующий метод. Сначала посмотрите на метод createOrUpdateIndex.В ElasticsearchRepository метод сохранения используется для обновления или создания нового, поэтому шаги в основном такие же.Я удалю оригинал (если есть) в новом типе. Ничто другое не отличается.

Затем мы обращаем внимание на два метода createOrUpdateIndex и removeIndex.На самом деле их очень просто создать или изменить.Вам нужно только запросить измененные данные, а затем преобразовать их в PostDocument, который можно напрямую сохранить.

  • com.example.service.impl.SearchServiceImpl
/**
 * 异步创建或者更新
 */
public void createOrUpdateIndex(PostMqIndexMessage message) {
    long postId = message.getPostId();
    PostVo postVo = postService.selectOne(new QueryWrapper<Post >().eq("p.id", postId));
    log.info("需要更新的post --------> {}",  postVo.toString());
    if(PostMqIndexMessage.CREATE.equals(message.getType())) {
        if(postRepository.existsById(postId)) {
            this.removeIndex(message);
        }
    }
    PostDocument postDocument = new PostDocument();
    modelMapper.map(postVo, postDocument);
    PostDocument saveDoc = postRepository.save(postDocument);
    log.info("es 索引更新成功!--> {}" , saveDoc.toString());
}

Удаление почти такое же, можно удалить прямо по id

@Override
public void removeIndex(PostMqIndexMessage message) {
    long postId = message.getPostId();
    postRepository.deleteById(postId);
    log.info("es 索引删除成功!--> {}" , message.toString());

}

Что касается потребительской стороны MQ, куда вы отправляете сообщения MQ? Отправлять сообщение MQ при добавлении измененных данных. Находим соответствующий метод:

  • com.example.controller.PostController#submit

Среди них отправляющий шаблон MQ является Amqptemplate, который может быть введен напрямую.

@Autowired
AmqpTemplate amqpTemplate;

Удаление также аналогично:

  • com.example.controller.PostController#delete

После отправки mq потребитель синхронизирует данные es, поэтому мы можем гарантировать согласованность данных es и данных базы данных в режиме реального времени.

На этом функция поиска завершена.

Закулисное управление

Избранное

Такие функции, как выбор и удаление статей - это все действия, которыми может управлять фоновый администратор.На самом деле обозначение и выбор - это вопрос статуса поста, поэтому логика очень проста, а оригинальный js уже написал метод для нас.Так что нам просто нужно изменить ссылку и добавить соответствующую логику обработки в AdminController.

Давайте сначала посмотрим на переднюю часть:

  • static/res/mods/jie.js
//设置置顶、状态
,set: function(div){
  var othis = $(this);
  fly.json('/admin/jie-set/', {
    id: div.data('id')
    ,rank: othis.attr('rank')
    ,field: othis.attr('field')
  }, function(res){
    if(res.status === 0){
      location.reload();
    }
  });
}

Как видите, есть несколько параметров при инициации ссылки, id, rank, field и т.д. Затем посмотрите на html:

  • templates/post/view.ftl
<@shiro.hasRole >
    <span class="layui-btn layui-btn-xs jie-admin" type="set" field="delete" rank="1">删除</span>
    <#if post.level == 0><span class="layui-btn layui-btn-xs jie-admin" type="set" field="stick" rank="1">置顶</span></#if>
    <#if post.level gt 0><span class="layui-btn layui-btn-xs jie-admin" type="set" field="stick" rank="0" style="background-color:#ccc;">取消置顶</span></#if>
    <#if !post.recommend><span class="layui-btn layui-btn-xs jie-admin" type="set" field="status" rank="1">加精</span></#if>
    <#if post.recommend><span class="layui-btn layui-btn-xs jie-admin" type="set" field="status" rank="0" style="background-color:#ccc;">取消加精</span></#if>
</@shiro.hasRole>

Поскольку это операция фонового управления, требуется разрешение администратора <@shiro.hasrole>, а затем нам нужно объявить проблему разрешений в сфере Широ.Здесь я относительно просто, без написания модуля разрешений, напрямую пишу admin как член высшего руководства:

  • com.example.shiro.AccountRealm
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    AccountProfile principal = (AccountProfile) principalCollection.getPrimaryPrincipal();
    // 硬编码
    if(principal.getUsername().equals("admin") || principal.getId() == 1){
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRole("admin");
        return info;
    }
    return null;
}

Вы можете видеть, что пользователь admin жестко запрограммирован или id равен 1 с ролью admin, а остальные не имеют никаких разрешений и ролей. Затем мы смотрим на AdminController

  • com.example.controller.AdminController
@ResponseBody
@PostMapping("/jie-set")
public Result jieSet(Long id, Integer rank, String field) {
    Post post = postService.getById(id);
    Assert.isTrue(post != null, "该文章已被删除");
    if("delete".equals(field)) {
        postService.removeById(id);
        return Result.succ(null);
    } else if("stick".equals(field)) {
        post.setLevel(rank);
    } else if("status".equals(field)) {
        post.setRecommend(rank == 1);
    }
    postService.updateById(post);
    return Result.succ(null);
}

Просто вызовите updateById, чтобы изменить состояние. Здесь же есть функция удаления, а это значит, что не только я могу удалять, но и суперадминистратор тоже имеет право удалять статьи.