├── web ├── .browserslistrc ├── public │ ├── favicon.ico │ ├── image │ │ ├── cover1.png │ │ └── cover2.png │ ├── js │ │ └── session-storage.js │ └── index.html ├── src │ ├── assets │ │ ├── logo.png │ │ └── fairy-wiki.png │ ├── views │ │ ├── about.vue │ │ ├── home.vue │ │ ├── doc.vue │ │ └── admin │ │ │ └── admin-category.vue │ ├── shims-vue.d.ts │ ├── store │ │ └── index.ts │ ├── App.vue │ ├── models.ts │ ├── main.ts │ ├── util │ │ └── tool.ts │ ├── components │ │ ├── the-footer.vue │ │ └── the-header.vue │ └── router │ │ └── index.ts ├── .env.dev ├── .env.prod ├── .gitignore ├── README.md ├── .eslintrc.js ├── tsconfig.json └── package.json ├── http-requests ├── hello.http ├── mysql-test-list.http └── ebook.http ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── src ├── main │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── yubincloud │ │ │ └── fairywiki │ │ │ ├── domain │ │ │ ├── Test.java │ │ │ ├── Demo.java │ │ │ ├── Content.java │ │ │ ├── Category.java │ │ │ ├── User.java │ │ │ ├── Doc.java │ │ │ ├── EbookSnapshot.java │ │ │ └── Ebook.java │ │ │ ├── dto │ │ │ ├── req │ │ │ │ ├── DocQueryReqDto.java │ │ │ │ ├── CategoryQueryReqDto.java │ │ │ │ ├── CategorySaveReqDto.java │ │ │ │ ├── UserQueryReqDto.java │ │ │ │ ├── EbookQueryReqDto.java │ │ │ │ ├── DocDeleteReqDto.java │ │ │ │ ├── PageReqDto.java │ │ │ │ ├── UserResetPwdReqDto.java │ │ │ │ ├── UserLoginReqDto.java │ │ │ │ ├── EbookSaveReqDto.java │ │ │ │ ├── UserSaveReqDto.java │ │ │ │ └── DocSaveReqDto.java │ │ │ └── resp │ │ │ │ ├── PageRespDto.java │ │ │ │ ├── CategoryQueryRespDto.java │ │ │ │ ├── UserLoginRespDto.java │ │ │ │ ├── UserQueryRespDto.java │ │ │ │ ├── ErrorCode.java │ │ │ │ ├── DocQueryRespDto.java │ │ │ │ ├── RestfulModel.java │ │ │ │ ├── StatisticRespDto.java │ │ │ │ └── EbookQueryRespDto.java │ │ │ ├── mapper │ │ │ ├── TestMapper.java │ │ │ ├── EbookSnapshotMapperCustom.java │ │ │ ├── DocMapperCustom.java │ │ │ ├── DocMapper.java │ │ │ ├── UserMapper.java │ │ │ ├── DemoMapper.java │ │ │ ├── CategoryMapper.java │ │ │ ├── EbookMapper.java │ │ │ ├── EbookSnapshotMapper.java │ │ │ └── ContentMapper.java │ │ │ ├── config │ │ │ ├── WebSocketConfig.java │ │ │ ├── CorsConfig.java │ │ │ ├── SpringMvcConfig.java │ │ │ ├── JacksonConfig.java │ │ │ └── Swagger3Config.java │ │ │ ├── utils │ │ │ ├── RequestContext.java │ │ │ ├── RedisUtil.java │ │ │ ├── CopyUtil.java │ │ │ └── SnowFlake.java │ │ │ ├── service │ │ │ ├── TestService.java │ │ │ ├── WsService.java │ │ │ ├── EbookSnapshotService.java │ │ │ ├── EbookService.java │ │ │ ├── CategoryService.java │ │ │ ├── UserService.java │ │ │ └── DocService.java │ │ │ ├── exception │ │ │ ├── BusinessExceptionCode.java │ │ │ └── BusinessException.java │ │ │ ├── controller │ │ │ ├── TestController.java │ │ │ ├── EbookSnapshotController.java │ │ │ ├── ControllerExceptionHandler.java │ │ │ ├── EbookController.java │ │ │ ├── CategoryController.java │ │ │ ├── DocController.java │ │ │ └── UserController.java │ │ │ ├── rocketmq │ │ │ └── VoteTopicConsumer.java │ │ │ ├── job │ │ │ ├── DocJob.java │ │ │ ├── EbookSnapshotJob.java │ │ │ └── ScheduleJobDemo.java │ │ │ ├── FairyWikiApplication.java │ │ │ ├── filter │ │ │ └── LogFilter.java │ │ │ ├── interceptor │ │ │ ├── LogInterceptor.java │ │ │ └── LoginInterceptor.java │ │ │ ├── websocket │ │ │ └── WebSocketServer.java │ │ │ └── aspect │ │ │ └── LogAspect.java │ └── resources │ │ ├── banner.txt │ │ ├── mapper │ │ ├── TestMapper.xml │ │ ├── DocMapperCustom.xml │ │ ├── EbookSnapshotMapperCustom.xml │ │ ├── DemoMapper.xml │ │ ├── ContentMapper.xml │ │ ├── CategoryMapper.xml │ │ └── UserMapper.xml │ │ ├── application.yml │ │ ├── generator │ │ └── generator-config.xml │ │ └── logback-spring.xml └── test │ └── java │ └── io │ └── github │ └── yubincloud │ └── fairywiki │ └── FairyWikiApplicationTests.java ├── sql-scripts ├── create_db.sql └── create_tables.sql ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── mvnw.cmd /web/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yubinCloud/fairy-wiki/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yubinCloud/fairy-wiki/HEAD/web/src/assets/logo.png -------------------------------------------------------------------------------- /http-requests/hello.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8880/test/hello 2 | Accept: application/json 3 | 4 | 5 | ### 6 | -------------------------------------------------------------------------------- /web/public/image/cover1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yubinCloud/fairy-wiki/HEAD/web/public/image/cover1.png -------------------------------------------------------------------------------- /web/public/image/cover2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yubinCloud/fairy-wiki/HEAD/web/public/image/cover2.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yubinCloud/fairy-wiki/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /http-requests/mysql-test-list.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8880/test/list 2 | Accept: application/json 3 | 4 | ### 5 | -------------------------------------------------------------------------------- /web/src/assets/fairy-wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yubinCloud/fairy-wiki/HEAD/web/src/assets/fairy-wiki.png -------------------------------------------------------------------------------- /web/.env.dev: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VUE_APP_SERVER=http://127.0.0.1:8880 3 | VUE_APP_WS_SERVER=ws://127.0.0.1:8880 4 | -------------------------------------------------------------------------------- /web/.env.prod: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VUE_APP_SERVER=http://127.0.0.1:8880 3 | VUE_APP_WS_SERVER=ws://127.0.0.1:8880 4 | -------------------------------------------------------------------------------- /web/src/views/about.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/Test.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Test { 7 | private Integer id; 8 | private String name; 9 | private String password; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/DocQueryReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | @Data 7 | @EqualsAndHashCode(callSuper = true) 8 | public class DocQueryReqDto extends PageReqDto { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/PageRespDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class PageRespDto { 9 | 10 | private long total; 11 | 12 | private List list; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/CategoryQueryReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | 7 | @Data 8 | @EqualsAndHashCode(callSuper = true) 9 | public class CategoryQueryReqDto extends PageReqDto { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/CategorySaveReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class CategorySaveReqDto { 7 | private Long id; 8 | 9 | private Long parent; 10 | 11 | private String name; 12 | 13 | private Integer sort; 14 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/CategoryQueryRespDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class CategoryQueryRespDto { 7 | private Long id; 8 | 9 | private Long parent; 10 | 11 | private String name; 12 | 13 | private Integer sort; 14 | } -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/UserQueryReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | @Data 7 | @EqualsAndHashCode(callSuper = true) 8 | public class UserQueryReqDto extends PageReqDto { 9 | 10 | private String loginName; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/UserLoginRespDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UserLoginRespDto { 7 | 8 | private Long id; 9 | 10 | private String loginName; 11 | 12 | private String name; 13 | 14 | private String token; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/UserQueryRespDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UserQueryRespDto { 7 | private Long id; 8 | 9 | private String loginName; 10 | 11 | private String name; 12 | 13 | private String password; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/TestMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Test; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import java.util.List; 7 | 8 | @Repository 9 | public interface TestMapper { 10 | 11 | List list(); 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/io/github/yubincloud/fairywiki/FairyWikiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class FairyWikiApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /sql-scripts/create_db.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE wiki; 2 | 3 | create table test 4 | ( 5 | id int, 6 | name varchar(255) not null, 7 | password varchar(255) not null 8 | ); 9 | 10 | create unique index test_id_uindex 11 | on test (id); 12 | 13 | alter table test 14 | add constraint test_pk 15 | primary key (id); 16 | 17 | alter table test modify id int auto_increment; -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/EbookQueryReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | 7 | @Data 8 | @EqualsAndHashCode(callSuper = true) 9 | public class EbookQueryReqDto extends PageReqDto { 10 | 11 | private String name; 12 | 13 | private Long CategoryId2; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/DocDeleteReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.util.List; 7 | 8 | @Data 9 | public class DocDeleteReqDto { 10 | 11 | @NotNull(message = "ids 字段不能为 null") 12 | private List ids; // 所有需要被删除的文档id 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | public class ErrorCode { 4 | 5 | public static final int SUCCESS = 0; 6 | 7 | public static final int ARGS_VALIDATION_ERROR = 1000; 8 | 9 | public static final int UNKNOWN_ERROR = 1010; 10 | 11 | public static final int BUSINESS_EXCEPTION = -1001; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | Fairy Wiki System is booting... 2 | ___________ .__ __ __.__ __ .__ 3 | \_ _____/____ |__|______ ___.__. / \ / \__| | _|__| 4 | | __) \__ \ | \_ __ < | | \ \/\/ / | |/ / | 5 | | \ / __ \| || | \/\___ | \ /| | <| | 6 | \___ / (____ /__||__| / ____| \__/\ / |__|__|_ \__| 7 | \/ \/ \/ \/ \/ -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/EbookSnapshotMapperCustom.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.dto.resp.StatisticRespDto; 4 | 5 | import java.util.List; 6 | 7 | public interface EbookSnapshotMapperCustom { 8 | void genSnapshot(); 9 | 10 | List getStatistic(); 11 | 12 | List get30DayStatistic(); 13 | } 14 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # web 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/DocQueryRespDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class DocQueryRespDto { 7 | private Long id; 8 | 9 | private Long ebookId; 10 | 11 | private Long parent; 12 | 13 | private String name; 14 | 15 | private Integer sort; 16 | 17 | private Integer viewCount; 18 | 19 | private Integer voteCount; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/mapper/TestMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /log/ 2 | HELP.md 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/RestfulModel.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | public class RestfulModel { 9 | 10 | /** 11 | * 响应的错误码 12 | */ 13 | private int code; 14 | 15 | /** 16 | * 返回信息 17 | */ 18 | private String msg; 19 | 20 | /** 21 | * 返回泛型数据,自定义类型 22 | */ 23 | private T data; 24 | } 25 | -------------------------------------------------------------------------------- /web/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | declare let SessionStorage: any; 4 | const USER = 'USER'; 5 | 6 | const store = createStore({ 7 | state: { 8 | localUser: SessionStorage.get(USER) || {} // 表示当前登录的用户 9 | }, 10 | mutations: { 11 | setLocalUser(state, user) { 12 | state.localUser = user; 13 | SessionStorage.set(USER, user); // 将该用户的信息存放于 SessionStorage 中 14 | } 15 | }, 16 | actions: { 17 | }, 18 | modules: { 19 | } 20 | }) 21 | 22 | 23 | export default store; -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.server.standard.ServerEndpointExporter; 6 | 7 | @Configuration 8 | public class WebSocketConfig { 9 | 10 | @Bean 11 | public ServerEndpointExporter serverEndpointExporter() { 12 | return new ServerEndpointExporter(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/StatisticRespDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | @Data 9 | public class StatisticRespDto { 10 | 11 | @JsonFormat(pattern="MM-dd", timezone = "GMT+8") 12 | private Date date; 13 | 14 | private int viewCount; 15 | 16 | private int voteCount; 17 | 18 | private int viewIncrease; 19 | 20 | private int voteIncrease; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/resp/EbookQueryRespDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.resp; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class EbookQueryRespDto { 7 | private Long id; 8 | 9 | private String name; 10 | 11 | private Long category1Id; 12 | 13 | private Long category2Id; 14 | 15 | private String description; 16 | 17 | private String cover; 18 | 19 | private Integer docCount; 20 | 21 | private Integer viewCount; 22 | 23 | private Integer voteCount; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/utils/RequestContext.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.utils; 2 | 3 | import java.io.Serializable; 4 | 5 | public class RequestContext implements Serializable { 6 | 7 | private static final ThreadLocal remoteAddr = new ThreadLocal<>(); 8 | 9 | public static String getRemoteAddr() { 10 | return remoteAddr.get(); 11 | } 12 | 13 | public static void setRemoteAddr(String remoteAddr) { 14 | RequestContext.remoteAddr.set(remoteAddr); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /web/public/js/session-storage.js: -------------------------------------------------------------------------------- 1 | SessionStorage = { 2 | get: function (key) { 3 | const v = sessionStorage.getItem(key); 4 | if (v && typeof(v) !== "undefined" && v !== "undefined") { 5 | return JSON.parse(v); 6 | } 7 | }, 8 | set: function (key, data) { 9 | sessionStorage.setItem(key, JSON.stringify(data)); 10 | }, 11 | remove: function (key) { 12 | sessionStorage.removeItem(key); 13 | }, 14 | clearAll: function () { 15 | sessionStorage.clear(); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/PageReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.Max; 6 | import javax.validation.constraints.NotNull; 7 | 8 | /** 9 | * 当需要对数据库的查询进行分页时需要继承此 Req 类 10 | */ 11 | @Data 12 | public class PageReqDto { 13 | 14 | @NotNull(message = "【页码】不能为空") 15 | private int pageNum; 16 | 17 | @NotNull(message = "【每页条数】不能为空") 18 | @Max(value = 1000, message = "【每页条数】不能超过1000") 19 | private int pageSize; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/UserResetPwdReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import javax.validation.constraints.Pattern; 7 | 8 | @Data 9 | public class UserResetPwdReqDto { 10 | @NotNull(message = "【用户名】不能为空") 11 | Long id; 12 | 13 | @NotNull(message = "【密码】不能为空") 14 | @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】至少包含 数字和英文,长度6-32") 15 | private String password; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/UserLoginReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotEmpty; 6 | import javax.validation.constraints.Pattern; 7 | 8 | @Data 9 | public class UserLoginReqDto { 10 | 11 | @NotEmpty(message = "【用户名】不能为空") 12 | private String loginName; 13 | 14 | @NotEmpty(message = "【密码】不能为空") 15 | @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】规则不正确") 16 | private String password; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/service/TestService.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.service; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Test; 4 | import io.github.yubincloud.fairywiki.mapper.TestMapper; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | public class TestService { 12 | 13 | @Autowired 14 | private TestMapper testMapper; 15 | 16 | public List list() { 17 | return testMapper.list(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/DocMapperCustom.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import org.apache.ibatis.annotations.Param; 4 | 5 | public interface DocMapperCustom { 6 | 7 | /** 8 | * view count 字段递增一次 9 | * @param docId 文档的 id 10 | */ 11 | void increaseViewCount(@Param("id") Long docId); 12 | 13 | /** 14 | * vote count 字段递增一次 15 | * @param docId 文档的 id 16 | */ 17 | void increaseVoteCount(@Param("id") Long docId); 18 | 19 | /** 20 | * 更新所有 Ebook 的阅读量、点赞量信息 21 | */ 22 | void updateEbookFooter(); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/EbookSaveReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | @Data 8 | public class EbookSaveReqDto { 9 | private Long id; 10 | 11 | @NotNull(message = "Ebook name 不能为空") 12 | private String name; 13 | 14 | private Long category1Id; 15 | 16 | private Long category2Id; 17 | 18 | private String description; 19 | 20 | private String cover; 21 | 22 | private Integer docCount = 0; 23 | 24 | private Integer viewCount = 0; 25 | 26 | private Integer voteCount = 0; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/exception/BusinessExceptionCode.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.exception; 2 | 3 | /** 4 | * 枚举业务异常的类型 5 | */ 6 | public enum BusinessExceptionCode { 7 | 8 | USER_LOGIN_NAME_EXIST("登录名已存在"), 9 | LOGIN_USER_ERROR("用户名不存在或密码错误"), 10 | VOTE_REPEAT("您已点赞过,请于 24 小时后再次操作"), 11 | ; 12 | 13 | private String desc; 14 | 15 | BusinessExceptionCode(String desc) { 16 | this.desc = desc; 17 | } 18 | 19 | public String getDesc() { 20 | return desc; 21 | } 22 | 23 | public void setDesc(String desc) { 24 | this.desc = desc; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /http-requests/ebook.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8880/ebook/save 2 | Content-Type: application/json 3 | 4 | { 5 | "cover": "12", 6 | "description": "FastAPI framework, high performance, easy to learn, fast to code, ready for production", 7 | "name": "fastapi 入门教程" 8 | } 9 | 10 | ### 11 | GET http://localhost:8880/ebook/list 12 | Accept: application/json 13 | 14 | ### 15 | 16 | 17 | GET http://localhost:8880/ebook/list?name=Spring 18 | Accept: application/json 19 | 20 | ### 21 | 22 | GET http://localhost:8880/ebook/list 23 | Accept: application/json 24 | 25 | ### 26 | 27 | GET http://localhost:8880/ebook/list?pageNum=1&pageSize=3 28 | Accept: application/json 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/UserSaveReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import javax.validation.constraints.Pattern; 7 | 8 | @Data 9 | public class UserSaveReqDto { 10 | private Long id; 11 | 12 | @NotNull(message = "【用户名】不能为空") 13 | private String loginName; 14 | 15 | @NotNull(message = "【昵称】不能为空") 16 | private String name; 17 | 18 | @NotNull(message = "【密码】不能为空") 19 | @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】至少包含 数字和英文,长度6-32") 20 | private String password; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | 'vue/no-unused-components': 'off', 18 | '@typescript-eslint/no-explicit-any': 0, 19 | 'vue/no-unused-vars': 0, 20 | '@typescript-eslint/no-unused-vars': 0, 21 | '@typescript-eslint/explicit-module-boundary-types': 0, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/dto/req/DocSaveReqDto.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.dto.req; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | @Data 8 | public class DocSaveReqDto { 9 | private Long id; 10 | 11 | @NotNull(message = "【电子书】不能为空") 12 | private Long ebookId; 13 | 14 | @NotNull(message = "【父文档】不能为空") 15 | private Long parent; 16 | 17 | @NotNull(message = "【名称】不能为空") 18 | private String name; 19 | 20 | @NotNull(message = "【顺序】不能为空") 21 | private Integer sort; 22 | 23 | private Integer viewCount; 24 | 25 | private Integer voteCount; 26 | 27 | @NotNull(message = "【内容】不能为空") 28 | private String content; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # 应用名称 2 | spring: 3 | application: 4 | name: fairy-wiki 5 | # MySQL 配置 6 | datasource: 7 | url: jdbc:mysql://127.0.0.1/wiki?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT&useSSL=true&allowMultiQueries=true 8 | username: root 9 | password: yubin3869 10 | driver-class-name: com.mysql.cj.jdbc.Driver 11 | # Redis 配置 12 | redis: 13 | host: localhost 14 | port: 6379 15 | database: 1 16 | 17 | # 应用服务 WEB 访问端口 18 | server.port: 8880 19 | 20 | 21 | # 配置mybatis所有Mapper.xml所在的路径 22 | mybatis: 23 | mapper-locations: classpath:/mapper/**/*.xml 24 | 25 | # RocketMQ 配置 26 | rocketmq: 27 | name-server: 127.0.0.1:9876 28 | producer: 29 | group: default 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.exception; 2 | 3 | 4 | /** 5 | * 业务异常,自定义的非 bug 异常 6 | */ 7 | public class BusinessException extends RuntimeException{ 8 | 9 | private BusinessExceptionCode code; 10 | 11 | public BusinessException (BusinessExceptionCode code) { 12 | super(code.getDesc()); 13 | this.code = code; 14 | } 15 | 16 | public BusinessExceptionCode getCode() { 17 | return code; 18 | } 19 | 20 | public void setCode(BusinessExceptionCode code) { 21 | this.code = code; 22 | } 23 | 24 | /** 25 | * 不写入堆栈信息,提高性能 26 | */ 27 | @Override 28 | public Throwable fillInStackTrace() { 29 | return this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/service/WsService.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.service; 2 | 3 | import io.github.yubincloud.fairywiki.websocket.WebSocketServer; 4 | import org.slf4j.MDC; 5 | import org.springframework.scheduling.annotation.Async; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.annotation.Resource; 9 | 10 | @Service 11 | public class WsService { 12 | 13 | @Resource 14 | private WebSocketServer webSocketServer; 15 | 16 | /** 17 | * 以异步的方式将信息通过 websocket 发送给前端 18 | * @param info 所要发送的信息 19 | * @param logId LOG 的 id,使得本次异步所开的线程也能与原先的线程记录在同一个 LOG 号下,方便运维查找 20 | */ 21 | @Async 22 | public void sendInfo(String info, String logId) { 23 | MDC.put("LOG_ID", logId); 24 | webSocketServer.sendInfo(info); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.controller; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Test; 4 | import io.github.yubincloud.fairywiki.service.TestService; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import javax.annotation.Resource; 10 | import java.util.List; 11 | 12 | @RestController 13 | @RequestMapping("/test") 14 | public class TestController { 15 | 16 | @Resource 17 | private TestService testService; 18 | 19 | @GetMapping("/hello") 20 | public String hello() { 21 | return "Hello World!"; 22 | } 23 | 24 | @GetMapping("/list") 25 | public List list() { 26 | return testService.list(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/Demo.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | public class Demo { 4 | private Integer id; 5 | 6 | private String name; 7 | 8 | public Integer getId() { 9 | return id; 10 | } 11 | 12 | public void setId(Integer id) { 13 | this.id = id; 14 | } 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | StringBuilder sb = new StringBuilder(); 27 | sb.append(getClass().getSimpleName()); 28 | sb.append(" ["); 29 | sb.append("Hash = ").append(hashCode()); 30 | sb.append(", id=").append(id); 31 | sb.append(", name=").append(name); 32 | sb.append("]"); 33 | return sb.toString(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.cors.CorsConfiguration; 5 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | 9 | /** 10 | * 配置后端使其允许跨域 11 | */ 12 | @Configuration 13 | public class CorsConfig implements WebMvcConfigurer { 14 | 15 | @Override 16 | public void addCorsMappings(CorsRegistry registry) { 17 | registry.addMapping("/**") 18 | .allowedOriginPatterns("*") 19 | .allowedHeaders(CorsConfiguration.ALL) 20 | .allowedMethods(CorsConfiguration.ALL) 21 | .allowCredentials(true) 22 | .maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/Content.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | public class Content { 4 | private Long id; 5 | 6 | private String content; 7 | 8 | public Long getId() { 9 | return id; 10 | } 11 | 12 | public void setId(Long id) { 13 | this.id = id; 14 | } 15 | 16 | public String getContent() { 17 | return content; 18 | } 19 | 20 | public void setContent(String content) { 21 | this.content = content; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | StringBuilder sb = new StringBuilder(); 27 | sb.append(getClass().getSimpleName()); 28 | sb.append(" ["); 29 | sb.append("Hash = ").append(hashCode()); 30 | sb.append(", id=").append(id); 31 | sb.append(", content=").append(content); 32 | sb.append("]"); 33 | return sb.toString(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/DocMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Doc; 4 | import io.github.yubincloud.fairywiki.domain.DocExample; 5 | import java.util.List; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | public interface DocMapper { 9 | long countByExample(DocExample example); 10 | 11 | int deleteByExample(DocExample example); 12 | 13 | int deleteByPrimaryKey(Long id); 14 | 15 | int insert(Doc record); 16 | 17 | int insertSelective(Doc record); 18 | 19 | List selectByExample(DocExample example); 20 | 21 | Doc selectByPrimaryKey(Long id); 22 | 23 | int updateByExampleSelective(@Param("record") Doc record, @Param("example") DocExample example); 24 | 25 | int updateByExample(@Param("record") Doc record, @Param("example") DocExample example); 26 | 27 | int updateByPrimaryKeySelective(Doc record); 28 | 29 | int updateByPrimaryKey(Doc record); 30 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.User; 4 | import io.github.yubincloud.fairywiki.domain.UserExample; 5 | import java.util.List; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | public interface UserMapper { 9 | long countByExample(UserExample example); 10 | 11 | int deleteByExample(UserExample example); 12 | 13 | int deleteByPrimaryKey(Long id); 14 | 15 | int insert(User record); 16 | 17 | int insertSelective(User record); 18 | 19 | List selectByExample(UserExample example); 20 | 21 | User selectByPrimaryKey(Long id); 22 | 23 | int updateByExampleSelective(@Param("record") User record, @Param("example") UserExample example); 24 | 25 | int updateByExample(@Param("record") User record, @Param("example") UserExample example); 26 | 27 | int updateByPrimaryKeySelective(User record); 28 | 29 | int updateByPrimaryKey(User record); 30 | } -------------------------------------------------------------------------------- /src/main/resources/mapper/DocMapperCustom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UPDATE doc 7 | SET view_count = view_count + 1 8 | WHERE id = #{id} 9 | 10 | 11 | 12 | UPDATE doc 13 | SET vote_count = vote_count + 1 14 | WHERE id = #{id} 15 | 16 | 17 | 18 | UPDATE ebook t1, ( 19 | SELECT ebook_id, count(1) doc_count, sum(view_count) view_count, sum(vote_count) vote_count 20 | FROM doc 21 | GROUP BY ebook_id) t2 22 | SET t1.doc_count = t2.doc_count, t1.view_count = t2.view_count, t1.vote_count = t2.vote_count 23 | WHERE t1.id = t2.ebook_id 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/DemoMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Demo; 4 | import io.github.yubincloud.fairywiki.domain.DemoExample; 5 | import java.util.List; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | public interface DemoMapper { 9 | long countByExample(DemoExample example); 10 | 11 | int deleteByExample(DemoExample example); 12 | 13 | int deleteByPrimaryKey(Integer id); 14 | 15 | int insert(Demo record); 16 | 17 | int insertSelective(Demo record); 18 | 19 | List selectByExample(DemoExample example); 20 | 21 | Demo selectByPrimaryKey(Integer id); 22 | 23 | int updateByExampleSelective(@Param("record") Demo record, @Param("example") DemoExample example); 24 | 25 | int updateByExample(@Param("record") Demo record, @Param("example") DemoExample example); 26 | 27 | int updateByPrimaryKeySelective(Demo record); 28 | 29 | int updateByPrimaryKey(Demo record); 30 | } -------------------------------------------------------------------------------- /web/src/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 电子书 3 | */ 4 | export interface Ebook { 5 | id: string; 6 | name: string; 7 | category1Id: string; 8 | category2Id: string; 9 | description: string; 10 | cover: string; 11 | docCount: number; 12 | viewCount: number; 13 | voteCount: number; 14 | } 15 | 16 | /** 17 | * 电子书的查询表单类 18 | */ 19 | export interface EbookQueryForm { 20 | name: string; 21 | } 22 | 23 | /** 24 | * 电子书分类 25 | */ 26 | export interface Category { 27 | id: string; 28 | name: string; 29 | parent: string; 30 | sort: number; 31 | } 32 | 33 | /** 34 | * 电子书分类的查询类 35 | */ 36 | export interface CategoryQueryForm { 37 | name: string; 38 | } 39 | 40 | 41 | /** 42 | * 文档类 43 | */ 44 | export interface Doc { 45 | id: string; 46 | ebookId: string|null; 47 | parent: string; 48 | name: string; 49 | sort: number; 50 | viewCount: number; 51 | voteCount: number; 52 | content: string; 53 | } 54 | 55 | /** 56 | * 文档的查询类 57 | */ 58 | export interface DocQueryForm { 59 | name: string; 60 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/CategoryMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Category; 4 | import io.github.yubincloud.fairywiki.domain.CategoryExample; 5 | import java.util.List; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | public interface CategoryMapper { 9 | long countByExample(CategoryExample example); 10 | 11 | int deleteByExample(CategoryExample example); 12 | 13 | int deleteByPrimaryKey(Long id); 14 | 15 | int insert(Category record); 16 | 17 | int insertSelective(Category record); 18 | 19 | List selectByExample(CategoryExample example); 20 | 21 | Category selectByPrimaryKey(Long id); 22 | 23 | int updateByExampleSelective(@Param("record") Category record, @Param("example") CategoryExample example); 24 | 25 | int updateByExample(@Param("record") Category record, @Param("example") CategoryExample example); 26 | 27 | int updateByPrimaryKeySelective(Category record); 28 | 29 | int updateByPrimaryKey(Category record); 30 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/EbookMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Ebook; 4 | import io.github.yubincloud.fairywiki.domain.EbookExample; 5 | import java.util.List; 6 | import org.apache.ibatis.annotations.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface EbookMapper { 11 | long countByExample(EbookExample example); 12 | 13 | int deleteByExample(EbookExample example); 14 | 15 | int deleteByPrimaryKey(Long id); 16 | 17 | int insert(Ebook record); 18 | 19 | int insertSelective(Ebook record); 20 | 21 | List selectByExample(EbookExample example); 22 | 23 | Ebook selectByPrimaryKey(Long id); 24 | 25 | int updateByExampleSelective(@Param("record") Ebook record, @Param("example") EbookExample example); 26 | 27 | int updateByExample(@Param("record") Ebook record, @Param("example") EbookExample example); 28 | 29 | int updateByPrimaryKeySelective(Ebook record); 30 | 31 | int updateByPrimaryKey(Ebook record); 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 俞斌 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/rocketmq/VoteTopicConsumer.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.rocketmq; 2 | 3 | import io.github.yubincloud.fairywiki.websocket.WebSocketServer; 4 | import org.apache.rocketmq.common.message.MessageExt; 5 | import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; 6 | import org.apache.rocketmq.spring.core.RocketMQListener; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Service; 10 | 11 | import javax.annotation.Resource; 12 | 13 | @Service 14 | @RocketMQMessageListener(consumerGroup = "default", topic = "VOTE_TOPIC") 15 | public class VoteTopicConsumer implements RocketMQListener { 16 | 17 | private static final Logger LOG = LoggerFactory.getLogger(VoteTopicConsumer.class); 18 | 19 | @Resource 20 | private WebSocketServer webSocketServer; 21 | 22 | @Override 23 | public void onMessage(MessageExt messageExt) { 24 | byte[] body = messageExt.getBody(); 25 | LOG.info("ROCKETMQ 收到消息:{}", new String(body)); 26 | webSocketServer.sendInfo(new String(body)); 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/EbookSnapshotMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.EbookSnapshot; 4 | import io.github.yubincloud.fairywiki.domain.EbookSnapshotExample; 5 | import java.util.List; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | public interface EbookSnapshotMapper { 9 | long countByExample(EbookSnapshotExample example); 10 | 11 | int deleteByExample(EbookSnapshotExample example); 12 | 13 | int deleteByPrimaryKey(Long id); 14 | 15 | int insert(EbookSnapshot record); 16 | 17 | int insertSelective(EbookSnapshot record); 18 | 19 | List selectByExample(EbookSnapshotExample example); 20 | 21 | EbookSnapshot selectByPrimaryKey(Long id); 22 | 23 | int updateByExampleSelective(@Param("record") EbookSnapshot record, @Param("example") EbookSnapshotExample example); 24 | 25 | int updateByExample(@Param("record") EbookSnapshot record, @Param("example") EbookSnapshotExample example); 26 | 27 | int updateByPrimaryKeySelective(EbookSnapshot record); 28 | 29 | int updateByPrimaryKey(EbookSnapshot record); 30 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/utils/RedisUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.annotation.Resource; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | @Component 12 | public class RedisUtil { 13 | 14 | private static final Logger LOG = LoggerFactory.getLogger(RedisUtil.class); 15 | 16 | @Resource 17 | private RedisTemplate redisTemplate; 18 | 19 | /** 20 | * 检测 Redis 中的 key 是否重复,若不重复则存入该 key 21 | * @param second 过期时间(秒) 22 | * @return 重复则返回 false,若新增 key 则返回 true 23 | */ 24 | public boolean validateRepeatedKey(String key, long second) { 25 | if (redisTemplate.hasKey(key)) { 26 | LOG.info("key已存在:{}", key); 27 | return false; 28 | } else { 29 | LOG.info("key 不存在,放入 key:{},过期时间:{}秒", key, second); 30 | redisTemplate.opsForValue().set(key, key, second, TimeUnit.SECONDS); 31 | return true; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/job/DocJob.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.job; 2 | 3 | import io.github.yubincloud.fairywiki.service.DocService; 4 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.slf4j.MDC; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.Resource; 12 | 13 | @Component 14 | public class DocJob { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(DocJob.class); 17 | 18 | @Resource 19 | private DocService docService; 20 | 21 | @Resource 22 | private SnowFlake snowFlake; 23 | 24 | /** 25 | * 每30秒更新电子书信息 26 | */ 27 | @Scheduled(cron = "5/30 * * * * ?") 28 | public void cron() { 29 | // 增加日志流水号 30 | MDC.put("LOG_ID", String.valueOf(snowFlake.nextId())); 31 | LOG.info("更新电子书下的文档数据开始"); 32 | long start = System.currentTimeMillis(); 33 | docService.updateEbookFooter(); 34 | LOG.info("更新电子书下的文档数据结束,耗时:{}毫秒", System.currentTimeMillis() - start); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= htmlWebpackPlugin.options.title %> 11 | 12 | 13 | 16 |
17 |
26 | 首次加载会较慢,正在进入,请等待... 27 |
28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/utils/CopyUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.utils; 2 | 3 | import org.springframework.beans.BeanUtils; 4 | import org.springframework.util.CollectionUtils; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class CopyUtil { 10 | 11 | /** 12 | * 单体复制 13 | */ 14 | public static T copy(Object source, Class clazz) { 15 | if (source == null) { 16 | return null; 17 | } 18 | T obj; 19 | try { 20 | obj = clazz.newInstance(); 21 | } catch (Exception e) { 22 | e.printStackTrace(); 23 | return null; 24 | } 25 | BeanUtils.copyProperties(source, obj); 26 | return obj; 27 | } 28 | 29 | /** 30 | * 列表复制 31 | */ 32 | public static List copyList(List source, Class clazz) { 33 | List target = new ArrayList<>(); 34 | if (!CollectionUtils.isEmpty(source)){ 35 | for (Object c: source) { 36 | T obj = copy(c, clazz); 37 | target.add(obj); 38 | } 39 | } 40 | return target; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/FairyWikiApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.scheduling.annotation.EnableAsync; 10 | import org.springframework.scheduling.annotation.EnableScheduling; 11 | import springfox.documentation.oas.annotations.EnableOpenApi; 12 | 13 | 14 | @SpringBootApplication 15 | @MapperScan("io.github.yubincloud.fairywiki.mapper") 16 | @EnableOpenApi 17 | @EnableScheduling 18 | @EnableAsync 19 | public class FairyWikiApplication { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(FairyWikiApplication.class); 22 | 23 | public static void main(String[] args) { 24 | SpringApplication app = new SpringApplication(FairyWikiApplication.class); 25 | Environment env = app.run(args).getEnvironment(); 26 | LOG.info("启动成功!!"); 27 | LOG.info("地址: \thttp://127.0.0.1:{}", env.getProperty("server.port")); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve-dev": "vue-cli-service serve --mode dev --port 8080", 7 | "serve-prod": "vue-cli-service serve --mode prod", 8 | "build-dev": "vue-cli-service build --mode dev", 9 | "build-prod": "vue-cli-service build --mode prod", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons-vue": "^6.0.1", 14 | "ant-design-vue": "^2.1.2", 15 | "axios": "^0.21.0", 16 | "echarts": "^5.1.1", 17 | "vue": "^3.0.0", 18 | "vue-router": "^4.0.0-0", 19 | "vuex": "^4.0.0-0", 20 | "wangeditor": "^4.6.16" 21 | }, 22 | "devDependencies": { 23 | "@typescript-eslint/eslint-plugin": "^4.18.0", 24 | "@typescript-eslint/parser": "^4.18.0", 25 | "@vue/cli-plugin-eslint": "~4.5.0", 26 | "@vue/cli-plugin-router": "~4.5.0", 27 | "@vue/cli-plugin-typescript": "~4.5.0", 28 | "@vue/cli-plugin-vuex": "~4.5.0", 29 | "@vue/cli-service": "~4.5.0", 30 | "@vue/compiler-sfc": "^3.0.0", 31 | "@vue/eslint-config-typescript": "^7.0.0", 32 | "eslint": "^6.7.2", 33 | "eslint-plugin-vue": "^7.0.0", 34 | "typescript": "~4.1.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/config/SpringMvcConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.config; 2 | 3 | import io.github.yubincloud.fairywiki.interceptor.LoginInterceptor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | import javax.annotation.Resource; 9 | 10 | @Configuration 11 | public class SpringMvcConfig implements WebMvcConfigurer { 12 | 13 | @Resource 14 | LoginInterceptor loginInterceptor; 15 | 16 | public void addInterceptors(InterceptorRegistry registry) { 17 | registry.addInterceptor(loginInterceptor) 18 | .addPathPatterns("/**") 19 | .excludePathPatterns( 20 | "/test/**", 21 | "/redis/**", 22 | "/user/login", 23 | "/category/all", 24 | "/ebook/query", 25 | "/doc/all/**", 26 | "/doc/vote/**", 27 | "/doc/read-content/**", 28 | "/ebook-snapshot/**" 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/mapper/ContentMapper.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.mapper; 2 | 3 | import io.github.yubincloud.fairywiki.domain.Content; 4 | import io.github.yubincloud.fairywiki.domain.ContentExample; 5 | import java.util.List; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | public interface ContentMapper { 9 | long countByExample(ContentExample example); 10 | 11 | int deleteByExample(ContentExample example); 12 | 13 | int deleteByPrimaryKey(Long id); 14 | 15 | int insert(Content record); 16 | 17 | int insertSelective(Content record); 18 | 19 | List selectByExampleWithBLOBs(ContentExample example); 20 | 21 | List selectByExample(ContentExample example); 22 | 23 | Content selectByPrimaryKey(Long id); 24 | 25 | int updateByExampleSelective(@Param("record") Content record, @Param("example") ContentExample example); 26 | 27 | int updateByExampleWithBLOBs(@Param("record") Content record, @Param("example") ContentExample example); 28 | 29 | int updateByExample(@Param("record") Content record, @Param("example") ContentExample example); 30 | 31 | int updateByPrimaryKeySelective(Content record); 32 | 33 | int updateByPrimaryKeyWithBLOBs(Content record); 34 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/config/JacksonConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.module.SimpleModule; 5 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Primary; 10 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 11 | 12 | @Configuration 13 | public class JacksonConfig { 14 | 15 | @Bean 16 | @Primary 17 | @ConditionalOnMissingBean(ObjectMapper.class) 18 | public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) 19 | { 20 | ObjectMapper objectMapper = builder.createXmlMapper(false).build(); 21 | 22 | // 全局配置序列化返回 JSON 处理 23 | SimpleModule simpleModule = new SimpleModule(); 24 | //JSON Long ==> String 25 | simpleModule.addSerializer(Long.class, ToStringSerializer.instance); 26 | objectMapper.registerModule(simpleModule); 27 | return objectMapper; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/job/EbookSnapshotJob.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.job; 2 | 3 | import io.github.yubincloud.fairywiki.service.EbookSnapshotService; 4 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.slf4j.MDC; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.Resource; 12 | 13 | @Component 14 | public class EbookSnapshotJob { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(EbookSnapshotJob.class); 17 | 18 | @Resource 19 | private EbookSnapshotService ebookSnapshotService; 20 | 21 | @Resource 22 | private SnowFlake snowFlake; 23 | 24 | /** 25 | * 自定义cron表达式跑批 26 | * 只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过 27 | */ 28 | @Scheduled(cron = "0/59 0 0-12 * * ? ") 29 | public void doSnapshot() { 30 | // 增加日志流水号 31 | MDC.put("LOG_ID", String.valueOf(snowFlake.nextId())); 32 | LOG.info("生成今日电子书快照开始"); 33 | long start = System.currentTimeMillis(); 34 | ebookSnapshotService.genSnapshots(); 35 | LOG.info("生成今日电子书快照结束,耗时:{}毫秒", System.currentTimeMillis() - start); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fairy Wiki —— 知识库系统 2 | 3 | ![](https://img.shields.io/badge/license-MIT-000000.svg) ![](https://img.shields.io/badge/language-Java-orange.svg) ![](https://img.shields.io/badge/language-TypeScript-green.svg) 4 | 5 | ![Fairy2_small](https://gitee.com/yubinCloud/my-imgs-repo/raw/main/img/Fairy2_small.png) 6 | 7 | 可以在云端存储电子书、文档的知识库 Wiki 系统,一个由 **Spring Boot** + **Vue3** 搭建的全栈项目: 8 | 9 | + 前端 Vue CLI & Ant Design Vue 项目搭建 10 | + 后端 Spring Boot 搭建 11 | 12 | ## 界面设计 13 | 14 | ![FairyWikiDemo](https://gitee.com/yubinCloud/my-imgs-repo/raw/main/img/FairyWikiDemo.jpg) 15 | 16 | + 用户管理 17 | + 电子书管理 18 | + 文档管理 19 | + 分类管理 20 | + 富文本框的集成 21 | + 图形统计报表展示 22 | + .... 23 | 24 | ## 关键技术点 25 | 26 | + **axios** 解决前后端分离架构的通信问题 27 | + **AOP** 日志记录 28 | + **RocketMQ、WebSocket** 异步化实现消息通知 29 | + **ECharts** 用于数据统计展示 30 | + 定时任务设计 31 | + **Redis** 存储用户 token 和登陆校验 32 | + **Ant Design for Vue** 用于构建前端界面 33 | + 多环境配置文件分别用于开发和生产 34 | + **统一异常处理** 35 | + **拦截器**、**过滤器** 36 | + ...... 37 | 38 | ## 启动方式 39 | 40 | 需要分别启动前端和后端 41 | 42 | + 后端启动方式: 43 | 44 | + 使用 IDEA 打开后,安装 **lombok** 插件 45 | + 启动 Redis 46 | + 启动 RocketMQ 47 | + 以 Maven 方式运行该 Spring Boot 项目 48 | 49 | + 前端启动方式: 50 | 51 | + 在 /web 子目录下,运行一下命令: 52 | 53 | ```bash 54 | $ cnpm -- install 55 | ... 56 | $ cnpm run serve-dev 57 | ... 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import Antd from 'ant-design-vue' 6 | import 'ant-design-vue/dist/antd.css' 7 | import * as Icons from '@ant-design/icons-vue' 8 | import axios from 'axios'; 9 | import {Tool} from "@/util/tool"; 10 | 11 | axios.defaults.baseURL = process.env.VUE_APP_SERVER; // 在使用 axios 发送请求时全局的base域 12 | 13 | /** 14 | * axios 拦截器 15 | */ 16 | axios.interceptors.request.use(function (reqConf) { 17 | console.log('请求参数:', reqConf); 18 | const token = store.state.localUser.token; 19 | if (Tool.isNotEmpty(token)) { 20 | reqConf.headers.token = token; 21 | console.log("请求headers增加token:", token); 22 | } 23 | return reqConf; 24 | }, error => { 25 | return Promise.reject(error); 26 | }); 27 | 28 | axios.interceptors.response.use(function (resp) { 29 | console.log('返回结果:', resp); 30 | return resp; 31 | }, error => { 32 | console.log('返回错误:', error); 33 | return Promise.reject(error); 34 | }) 35 | 36 | 37 | const app = createApp(App); 38 | app.use(store).use(router).use(Antd).mount('#app'); 39 | 40 | // 全局使用 Ant Design 的图标库 41 | const icons: any = Icons; 42 | for (const i in icons) { 43 | app.component(i, icons[i]); 44 | } 45 | 46 | 47 | console.log('环境:', process.env.NODE_ENV) -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/job/ScheduleJobDemo.java: -------------------------------------------------------------------------------- 1 | //package io.github.yubincloud.fairywiki.job; 2 | // 3 | //import org.slf4j.Logger; 4 | //import org.slf4j.LoggerFactory; 5 | //import org.springframework.scheduling.annotation.Scheduled; 6 | //import org.springframework.stereotype.Component; 7 | // 8 | //import java.text.SimpleDateFormat; 9 | //import java.util.Date; 10 | // 11 | // 12 | //@Component 13 | //public class ScheduleJobDemo { 14 | // 15 | // private static final Logger LOG = LoggerFactory.getLogger(ScheduleJobDemo.class); 16 | // 17 | // /** 18 | // * 固定时间间隔,fixedRate单位毫秒 19 | // */ 20 | // @Scheduled(fixedRate = 1000) 21 | // public void simple() throws InterruptedException { 22 | // SimpleDateFormat formatter = new SimpleDateFormat("mm:ss"); 23 | // String dateString = formatter.format(new Date()); 24 | // Thread.sleep(2000); 25 | // LOG.info("每隔5秒钟执行一次: {}", dateString); 26 | // } 27 | // 28 | // /** 29 | // * 自定义cron表达式跑批 30 | // * 只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过 31 | // */ 32 | // @Scheduled(cron = "*/1 * * * * ?") 33 | // public void cron() throws InterruptedException { 34 | // SimpleDateFormat formatter = new SimpleDateFormat("mm:ss SSS"); 35 | // String dateString = formatter.format(new Date()); 36 | // Thread.sleep(1500); 37 | // LOG.info("每隔1秒钟执行一次: {}", dateString); 38 | // } 39 | //} 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/Category.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | public class Category { 4 | private Long id; 5 | 6 | private Long parent; 7 | 8 | private String name; 9 | 10 | private Integer sort; 11 | 12 | public Long getId() { 13 | return id; 14 | } 15 | 16 | public void setId(Long id) { 17 | this.id = id; 18 | } 19 | 20 | public Long getParent() { 21 | return parent; 22 | } 23 | 24 | public void setParent(Long parent) { 25 | this.parent = parent; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public void setName(String name) { 33 | this.name = name; 34 | } 35 | 36 | public Integer getSort() { 37 | return sort; 38 | } 39 | 40 | public void setSort(Integer sort) { 41 | this.sort = sort; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | StringBuilder sb = new StringBuilder(); 47 | sb.append(getClass().getSimpleName()); 48 | sb.append(" ["); 49 | sb.append("Hash = ").append(hashCode()); 50 | sb.append(", id=").append(id); 51 | sb.append(", parent=").append(parent); 52 | sb.append(", name=").append(name); 53 | sb.append(", sort=").append(sort); 54 | sb.append("]"); 55 | return sb.toString(); 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/service/EbookSnapshotService.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.service; 2 | 3 | import io.github.yubincloud.fairywiki.dto.resp.StatisticRespDto; 4 | import io.github.yubincloud.fairywiki.mapper.EbookSnapshotMapperCustom; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.annotation.Resource; 8 | import java.util.List; 9 | 10 | @Service 11 | public class EbookSnapshotService { 12 | 13 | @Resource 14 | private EbookSnapshotMapperCustom ebookSnapshotMapperCustom; 15 | 16 | public void genSnapshots() { 17 | ebookSnapshotMapperCustom.genSnapshot(); 18 | } 19 | 20 | /** 21 | * 获取首页数值数据:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长 22 | */ 23 | public List getStatistic() { 24 | List statisticDataList = ebookSnapshotMapperCustom.getStatistic(); 25 | if (statisticDataList.size() < 2) { 26 | if (statisticDataList.isEmpty()) { 27 | statisticDataList.add(null); 28 | statisticDataList.add(null); 29 | } else { 30 | statisticDataList.add(0, null); 31 | } 32 | } 33 | return statisticDataList; 34 | } 35 | 36 | /** 37 | * 30天数值统计 38 | */ 39 | public List get30DayStatistic() { 40 | return ebookSnapshotMapperCustom.get30DayStatistic(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/filter/LogFilter.java: -------------------------------------------------------------------------------- 1 | //package io.github.yubincloud.fairywiki.filter; 2 | // 3 | //import org.slf4j.Logger; 4 | //import org.slf4j.LoggerFactory; 5 | //import org.springframework.stereotype.Component; 6 | // 7 | //import javax.servlet.*; 8 | //import javax.servlet.http.HttpServletRequest; 9 | //import java.io.IOException; 10 | // 11 | //@Component 12 | //public class LogFilter implements Filter { 13 | // 14 | // private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class); 15 | // 16 | // @Override 17 | // public void init(FilterConfig filterConfig) throws ServletException { 18 | // 19 | // } 20 | // 21 | // @Override 22 | // public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 23 | // // 打印请求信息 24 | // HttpServletRequest request = (HttpServletRequest) servletRequest; 25 | // LOG.info("------------- LogFilter 开始 -------------"); 26 | // LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod()); 27 | // LOG.info("远程地址: {}", request.getRemoteAddr()); 28 | // 29 | // long startTime = System.currentTimeMillis(); 30 | // filterChain.doFilter(servletRequest, servletResponse); 31 | // LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime); 32 | // } 33 | //} 34 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/config/Swagger3Config.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.config; 2 | 3 | import io.swagger.annotations.ApiOperation; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import springfox.documentation.builders.ApiInfoBuilder; 7 | import springfox.documentation.builders.PathSelectors; 8 | import springfox.documentation.builders.RequestHandlerSelectors; 9 | import springfox.documentation.service.ApiInfo; 10 | import springfox.documentation.service.Contact; 11 | import springfox.documentation.spi.DocumentationType; 12 | import springfox.documentation.spring.web.plugins.Docket; 13 | 14 | @Configuration 15 | public class Swagger3Config { 16 | @Bean 17 | public Docket createRestApi() { 18 | return new Docket(DocumentationType.OAS_30) 19 | .apiInfo(apiInfo()) 20 | .select() 21 | .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) 22 | .paths(PathSelectors.any()) 23 | .build(); 24 | } 25 | 26 | private ApiInfo apiInfo() { 27 | return new ApiInfoBuilder() 28 | .title("Fairy Wiki") 29 | .description("知识库系统") 30 | .contact(new Contact("yubin", "https://github.com/yubinCloud", "yubin_SkyWalker@yeah.net")) 31 | .version("1.0") 32 | .build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/User.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | public class User { 4 | private Long id; 5 | 6 | private String loginName; 7 | 8 | private String name; 9 | 10 | private String password; 11 | 12 | public Long getId() { 13 | return id; 14 | } 15 | 16 | public void setId(Long id) { 17 | this.id = id; 18 | } 19 | 20 | public String getLoginName() { 21 | return loginName; 22 | } 23 | 24 | public void setLoginName(String loginName) { 25 | this.loginName = loginName; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public void setName(String name) { 33 | this.name = name; 34 | } 35 | 36 | public String getPassword() { 37 | return password; 38 | } 39 | 40 | public void setPassword(String password) { 41 | this.password = password; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | StringBuilder sb = new StringBuilder(); 47 | sb.append(getClass().getSimpleName()); 48 | sb.append(" ["); 49 | sb.append("Hash = ").append(hashCode()); 50 | sb.append(", id=").append(id); 51 | sb.append(", loginName=").append(loginName); 52 | sb.append(", name=").append(name); 53 | sb.append(", password=").append(password); 54 | sb.append("]"); 55 | return sb.toString(); 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/interceptor/LogInterceptor.java: -------------------------------------------------------------------------------- 1 | //package io.github.yubincloud.fairywiki.interceptor; 2 | // 3 | //import org.slf4j.Logger; 4 | //import org.slf4j.LoggerFactory; 5 | //import org.springframework.stereotype.Component; 6 | //import org.springframework.web.servlet.HandlerInterceptor; 7 | //import org.springframework.web.servlet.ModelAndView; 8 | // 9 | //import javax.servlet.http.HttpServletRequest; 10 | //import javax.servlet.http.HttpServletResponse; 11 | // 12 | ///** 13 | // * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 14 | // */ 15 | //@Component 16 | //public class LogInterceptor implements HandlerInterceptor { 17 | // 18 | // private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class); 19 | // 20 | // @Override 21 | // public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 22 | // // 打印请求信息 23 | // LOG.info("------------- LogInterceptor 开始 -------------"); 24 | // LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod()); 25 | // LOG.info("远程地址: {}", request.getRemoteAddr()); 26 | // 27 | // long startTime = System.currentTimeMillis(); 28 | // request.setAttribute("requestStartTime", startTime); 29 | // return true; 30 | // } 31 | // 32 | // @Override 33 | // public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { 34 | // long startTime = (Long) request.getAttribute("requestStartTime"); 35 | // LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime); 36 | // } 37 | //} -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/controller/EbookSnapshotController.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.controller; 2 | 3 | 4 | import io.github.yubincloud.fairywiki.dto.resp.ErrorCode; 5 | import io.github.yubincloud.fairywiki.dto.resp.RestfulModel; 6 | import io.github.yubincloud.fairywiki.dto.resp.StatisticRespDto; 7 | import io.github.yubincloud.fairywiki.service.EbookSnapshotService; 8 | import io.swagger.annotations.Api; 9 | import io.swagger.annotations.ApiOperation; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import javax.annotation.Resource; 15 | import java.util.List; 16 | 17 | @RestController 18 | @RequestMapping("/ebook-snapshot") 19 | @Api("电子书快照管理") 20 | public class EbookSnapshotController { 21 | 22 | @Resource 23 | private EbookSnapshotService ebookSnapshotService; 24 | 25 | @GetMapping("/get-statistic") 26 | @ApiOperation(value = "从电子书快照中昨天和今天的获取统计数据") 27 | public RestfulModel> getStatistic() { 28 | List statisticRespDtoList = ebookSnapshotService.getStatistic(); 29 | return new RestfulModel<>(ErrorCode.SUCCESS, "", statisticRespDtoList); 30 | } 31 | 32 | @GetMapping("/get-30-statistic") 33 | @ApiOperation(value = "从电子书快照中获取近30天的统计数据") 34 | public RestfulModel> get30DayStatistic() { 35 | List statisticRespDtoList = ebookSnapshotService.get30DayStatistic(); 36 | return new RestfulModel<>(ErrorCode.SUCCESS, "", statisticRespDtoList); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/util/tool.ts: -------------------------------------------------------------------------------- 1 | export class Tool { 2 | /** 3 | * 空校验 null或""都返回true 4 | */ 5 | public static isEmpty (obj: any): boolean { 6 | if ((typeof obj === 'string')) { 7 | return !obj || obj.replace(/\s+/g, "") === "" 8 | } else { 9 | return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0); 10 | } 11 | } 12 | 13 | /** 14 | * 非空校验 15 | */ 16 | public static isNotEmpty (obj: any): boolean { 17 | return !this.isEmpty(obj); 18 | } 19 | 20 | /** 21 | * 对象复制 22 | * @param obj 23 | */ 24 | public static copy (obj: Record) { 25 | if (Tool.isNotEmpty(obj)) { 26 | return JSON.parse(JSON.stringify(obj)); 27 | } 28 | } 29 | 30 | /** 31 | * 使用递归将数组转为树形结构 32 | * 父ID属性为parent 33 | */ 34 | public static array2Tree (array: any, parentId: number) { 35 | if (Tool.isEmpty(array)) { 36 | return []; 37 | } 38 | 39 | const result = []; 40 | for (let i = 0; i < array.length; i++) { 41 | const c = array[i]; 42 | // console.log(Number(c.parent), Number(parentId)); 43 | if (Number(c.parent) === Number(parentId)) { 44 | result.push(c); 45 | 46 | // 递归查看当前节点对应的子节点 47 | const children = Tool.array2Tree(array, c.id); 48 | if (Tool.isNotEmpty(children)) { 49 | c.children = children; 50 | } 51 | } 52 | } 53 | return result; 54 | } 55 | 56 | /** 57 | * 随机生成[len]长度的[radix]进制数 58 | * @param len 59 | * @param radix 默认62 60 | * @returns {string} 61 | */ 62 | public static uuid (len: number, radix = 62) { 63 | const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 64 | const uuid = []; 65 | radix = radix || chars.length; 66 | 67 | for (let i = 0; i < len; i++) { 68 | uuid[i] = chars[0 | Math.random() * radix]; 69 | } 70 | 71 | return uuid.join(''); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /web/src/components/the-footer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/controller/ControllerExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.controller; 2 | 3 | import io.github.yubincloud.fairywiki.dto.resp.ErrorCode; 4 | import io.github.yubincloud.fairywiki.dto.resp.RestfulModel; 5 | import io.github.yubincloud.fairywiki.exception.BusinessException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.validation.BindException; 9 | import org.springframework.web.bind.annotation.ControllerAdvice; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.bind.annotation.ResponseBody; 12 | 13 | @ControllerAdvice 14 | public class ControllerExceptionHandler { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class); 17 | 18 | /** 19 | * 统一处理参数校验异常 20 | * @param e 捕捉到的异常 21 | */ 22 | @ExceptionHandler(value = BindException.class) 23 | @ResponseBody 24 | public RestfulModel validExceptionHandler(BindException e) { 25 | String exceptionMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); 26 | LOG.warn("参数校验失败:{}", exceptionMsg); 27 | return new RestfulModel<>(ErrorCode.ARGS_VALIDATION_ERROR, exceptionMsg, null); 28 | } 29 | 30 | /** 31 | * 统一业务处理 32 | * @param exc 异常 33 | */ 34 | @ExceptionHandler(value = BusinessException.class) 35 | @ResponseBody 36 | public RestfulModel validExceptionHandler(BusinessException exc) { 37 | String errorMsg = exc.getCode().getDesc(); 38 | LOG.warn("业务异常:{}", errorMsg); 39 | return new RestfulModel<>(ErrorCode.BUSINESS_EXCEPTION, errorMsg, null); 40 | } 41 | 42 | /** 43 | * 统一处理 Exception,此时表明程序出现 BUG 44 | * @param exc 异常 45 | */ 46 | @ExceptionHandler(value = Exception.class) 47 | @ResponseBody 48 | public RestfulModel validExceptionHandler(Exception exc) { 49 | LOG.error("系统异常", exc); 50 | String errorMsg = "系统出现异常,请联系管理员"; 51 | return new RestfulModel<>(ErrorCode.UNKNOWN_ERROR, errorMsg, null); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/controller/EbookController.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.controller; 2 | 3 | import io.github.yubincloud.fairywiki.dto.req.EbookQueryReqDto; 4 | import io.github.yubincloud.fairywiki.dto.req.EbookSaveReqDto; 5 | import io.github.yubincloud.fairywiki.dto.resp.EbookQueryRespDto; 6 | import io.github.yubincloud.fairywiki.dto.resp.ErrorCode; 7 | import io.github.yubincloud.fairywiki.dto.resp.PageRespDto; 8 | import io.github.yubincloud.fairywiki.dto.resp.RestfulModel; 9 | import io.github.yubincloud.fairywiki.service.EbookService; 10 | import io.swagger.annotations.Api; 11 | import io.swagger.annotations.ApiOperation; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.annotation.Resource; 15 | import javax.validation.Valid; 16 | 17 | @Api("电子书管理") 18 | @RestController 19 | @RequestMapping("/ebook") 20 | public class EbookController { 21 | 22 | @Resource 23 | private EbookService ebookService; 24 | 25 | /** 26 | * @param ebookQueryReqDto 查询条件的参数 27 | * @return 查询到的所有ebook 28 | */ 29 | @ApiOperation("对 ebook 进行查询的接口") 30 | @GetMapping("/query") 31 | public RestfulModel> queryEbooks(@Valid EbookQueryReqDto ebookQueryReqDto) { 32 | PageRespDto bookList = ebookService.queryEbooks(ebookQueryReqDto); 33 | return new RestfulModel<>(ErrorCode.SUCCESS, "", bookList); 34 | } 35 | 36 | /** 37 | * 根据请求的参数保存一个 ebook,若id非空则为更新,否则为新增 38 | */ 39 | @ApiOperation(value = "根据请求的参数保存一个 ebook", 40 | notes = "若id非空则为更新,否则为新增") 41 | @PostMapping("/save") 42 | public RestfulModel saveEbook(@RequestBody @Valid EbookSaveReqDto ebookSaveReqDto) { 43 | ebookService.save(ebookSaveReqDto); 44 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 45 | } 46 | 47 | @ApiOperation(value = "删除一个 ebook") 48 | @DeleteMapping("/delete/{ebookId}") 49 | public RestfulModel deleteEbook(@PathVariable Long ebookId) { 50 | ebookService.deleteOneEbook(ebookId); 51 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/generator/generator-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/Doc.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | public class Doc { 4 | private Long id; 5 | 6 | private Long ebookId; 7 | 8 | private Long parent; 9 | 10 | private String name; 11 | 12 | private Integer sort; 13 | 14 | private Integer viewCount; 15 | 16 | private Integer voteCount; 17 | 18 | public Long getId() { 19 | return id; 20 | } 21 | 22 | public void setId(Long id) { 23 | this.id = id; 24 | } 25 | 26 | public Long getEbookId() { 27 | return ebookId; 28 | } 29 | 30 | public void setEbookId(Long ebookId) { 31 | this.ebookId = ebookId; 32 | } 33 | 34 | public Long getParent() { 35 | return parent; 36 | } 37 | 38 | public void setParent(Long parent) { 39 | this.parent = parent; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public void setName(String name) { 47 | this.name = name; 48 | } 49 | 50 | public Integer getSort() { 51 | return sort; 52 | } 53 | 54 | public void setSort(Integer sort) { 55 | this.sort = sort; 56 | } 57 | 58 | public Integer getViewCount() { 59 | return viewCount; 60 | } 61 | 62 | public void setViewCount(Integer viewCount) { 63 | this.viewCount = viewCount; 64 | } 65 | 66 | public Integer getVoteCount() { 67 | return voteCount; 68 | } 69 | 70 | public void setVoteCount(Integer voteCount) { 71 | this.voteCount = voteCount; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | StringBuilder sb = new StringBuilder(); 77 | sb.append(getClass().getSimpleName()); 78 | sb.append(" ["); 79 | sb.append("Hash = ").append(hashCode()); 80 | sb.append(", id=").append(id); 81 | sb.append(", ebookId=").append(ebookId); 82 | sb.append(", parent=").append(parent); 83 | sb.append(", name=").append(name); 84 | sb.append(", sort=").append(sort); 85 | sb.append(", viewCount=").append(viewCount); 86 | sb.append(", voteCount=").append(voteCount); 87 | sb.append("]"); 88 | return sb.toString(); 89 | } 90 | } -------------------------------------------------------------------------------- /web/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' 2 | import Home from '../views/home.vue' 3 | import About from '../views/about.vue' 4 | import AdminEbook from '../views/admin/admin-ebook.vue' 5 | import AdminCategory from '../views/admin/admin-category.vue' 6 | import AdminDoc from '../views/admin/admin-doc.vue' 7 | import AdminUser from '../views/admin/admin-user.vue' 8 | import Doc from '../views/doc.vue' 9 | import store from '@/store' 10 | import {Tool} from "@/util/tool"; 11 | 12 | 13 | const routes: Array = [ 14 | { 15 | path: '/', 16 | name: 'Home', 17 | component: Home 18 | }, 19 | { 20 | path: '/doc', 21 | name: 'Doc', 22 | component: Doc 23 | }, 24 | { 25 | path: '/about', 26 | name: 'About', 27 | component: About 28 | }, 29 | { 30 | path: '/admin/user', 31 | name: 'AdminUser', 32 | component: AdminUser, 33 | meta: { 34 | loginRequire: true 35 | } 36 | }, 37 | { 38 | path: '/admin/ebook', 39 | name: 'AdminEbook', 40 | component: AdminEbook, 41 | meta: { 42 | loginRequire: true 43 | } 44 | }, 45 | { 46 | path: '/admin/category', 47 | name: 'AdminCategory', 48 | component: AdminCategory, 49 | meta: { 50 | loginRequire: true 51 | } 52 | }, 53 | { 54 | path: '/admin/doc', 55 | name: 'AdminDoc', 56 | component: AdminDoc, 57 | meta: { 58 | loginRequire: true 59 | } 60 | } 61 | ] 62 | 63 | const router = createRouter({ 64 | history: createWebHistory(process.env.BASE_URL), 65 | routes 66 | }) 67 | 68 | /** 69 | * 路由登录拦截 70 | * :param: to 所要进入的 URL 71 | * :param: from 来自的 URL 72 | * :param: next 用户接下来所要进入的 URL 73 | */ 74 | router.beforeEach((to, from, next) => { 75 | // 要不要对meta.loginRequire属性做监控拦截 76 | if (to.matched.some(function (item) { 77 | console.log(item, "是否需要登录校验:", item.meta.loginRequire); 78 | return item.meta.loginRequire 79 | })) { 80 | const localUser = store.state.localUser; // 上面部分验证通过的话则进入这块逻辑 81 | if (Tool.isEmpty(localUser)) { 82 | console.log("用户未登录!"); 83 | next('/'); // 如果用户未登录,则跳转至首页 84 | } else { 85 | next(); // 登录的话则继续访问 86 | } 87 | } else { 88 | next(); 89 | } 90 | }); 91 | 92 | 93 | export default router 94 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/controller/CategoryController.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.controller; 2 | 3 | import io.github.yubincloud.fairywiki.dto.req.CategoryQueryReqDto; 4 | import io.github.yubincloud.fairywiki.dto.req.CategorySaveReqDto; 5 | import io.github.yubincloud.fairywiki.dto.resp.CategoryQueryRespDto; 6 | import io.github.yubincloud.fairywiki.dto.resp.ErrorCode; 7 | import io.github.yubincloud.fairywiki.dto.resp.PageRespDto; 8 | import io.github.yubincloud.fairywiki.dto.resp.RestfulModel; 9 | import io.github.yubincloud.fairywiki.service.CategoryService; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import javax.annotation.Resource; 13 | import javax.validation.Valid; 14 | import java.util.List; 15 | 16 | @RestController 17 | @RequestMapping("/category") 18 | public class CategoryController { 19 | 20 | @Resource 21 | private CategoryService categoryService; 22 | 23 | /** 24 | * 获取全部 Category 的接口 25 | */ 26 | @GetMapping("/all") 27 | public RestfulModel> allCategories() { 28 | List categoryList = categoryService.fetchAllCategories(); 29 | return new RestfulModel<>(ErrorCode.SUCCESS, "", categoryList); 30 | } 31 | 32 | /** 33 | * 对 category 进行查询的接口 34 | * @param categoryQueryReqDto 查询条件的参数 35 | * @return 查询到的所有category 36 | */ 37 | @GetMapping("/query") 38 | public RestfulModel> queryCategorys(@Valid CategoryQueryReqDto categoryQueryReqDto) { 39 | PageRespDto bookList = categoryService.queryCategorys(categoryQueryReqDto); 40 | return new RestfulModel<>(ErrorCode.SUCCESS, "", bookList); 41 | } 42 | 43 | /** 44 | * 根据请求的参数保存一个 category,若id非空则为更新,否则为新增 45 | */ 46 | @PostMapping("/save") 47 | public RestfulModel saveCategory(@RequestBody @Valid CategorySaveReqDto categorySaveReqDto) { 48 | categoryService.save(categorySaveReqDto); 49 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 50 | } 51 | 52 | @DeleteMapping("/delete/{categoryId}") 53 | public RestfulModel deleteCategory(@PathVariable Long categoryId) { 54 | categoryService.deleteOneCategory(categoryId); 55 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/websocket/WebSocketServer.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.websocket; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.scheduling.annotation.Async; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.websocket.*; 9 | import javax.websocket.server.PathParam; 10 | import javax.websocket.server.ServerEndpoint; 11 | import java.io.IOException; 12 | import java.util.HashMap; 13 | 14 | @Component 15 | @ServerEndpoint("/ws/{token}") 16 | public class WebSocketServer { 17 | private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class); 18 | 19 | /** 20 | * 每个客户端一个token 21 | */ 22 | private String clientToken = ""; 23 | 24 | private static final HashMap sessionMap = new HashMap<>(); 25 | 26 | /** 27 | * 连接成功 28 | */ 29 | @OnOpen 30 | public void onOpen(Session session, @PathParam("token") String token) { 31 | sessionMap.put(token, session); 32 | this.clientToken = token; 33 | LOG.info("有新连接:token:{},session id:{},当前连接数:{}", token, session.getId(), sessionMap.size()); 34 | } 35 | 36 | /** 37 | * 连接关闭 38 | */ 39 | @OnClose 40 | public void onClose(Session session) { 41 | sessionMap.remove(this.clientToken); 42 | LOG.info("连接关闭,token:{},session id:{}!当前连接数:{}", this.clientToken, session.getId(), sessionMap.size()); 43 | } 44 | 45 | /** 46 | * 收到消息 47 | */ 48 | @OnMessage 49 | public void onMessage(String message, Session session) { 50 | LOG.info("收到消息:{},内容:{}", clientToken, message); 51 | } 52 | 53 | /** 54 | * 连接错误 55 | */ 56 | @OnError 57 | public void onError(Session session, Throwable error) { 58 | LOG.error("发生错误", error); 59 | } 60 | 61 | /** 62 | * 群发消息 63 | */ 64 | public void sendInfo(String message) { 65 | for (String token : sessionMap.keySet()) { 66 | Session session = sessionMap.get(token); 67 | try { 68 | session.getBasicRemote().sendText(message); 69 | } catch (IOException e) { 70 | LOG.error("推送消息失败:{},内容:{}", token, message); 71 | } 72 | LOG.info("推送消息:{},内容:{}", token, message); 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/EbookSnapshot.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | import java.util.Date; 4 | 5 | public class EbookSnapshot { 6 | private Long id; 7 | 8 | private Long ebookId; 9 | 10 | private Date date; 11 | 12 | private Integer viewCount; 13 | 14 | private Integer voteCount; 15 | 16 | private Integer viewIncrease; 17 | 18 | private Integer voteIncrease; 19 | 20 | public Long getId() { 21 | return id; 22 | } 23 | 24 | public void setId(Long id) { 25 | this.id = id; 26 | } 27 | 28 | public Long getEbookId() { 29 | return ebookId; 30 | } 31 | 32 | public void setEbookId(Long ebookId) { 33 | this.ebookId = ebookId; 34 | } 35 | 36 | public Date getDate() { 37 | return date; 38 | } 39 | 40 | public void setDate(Date date) { 41 | this.date = date; 42 | } 43 | 44 | public Integer getViewCount() { 45 | return viewCount; 46 | } 47 | 48 | public void setViewCount(Integer viewCount) { 49 | this.viewCount = viewCount; 50 | } 51 | 52 | public Integer getVoteCount() { 53 | return voteCount; 54 | } 55 | 56 | public void setVoteCount(Integer voteCount) { 57 | this.voteCount = voteCount; 58 | } 59 | 60 | public Integer getViewIncrease() { 61 | return viewIncrease; 62 | } 63 | 64 | public void setViewIncrease(Integer viewIncrease) { 65 | this.viewIncrease = viewIncrease; 66 | } 67 | 68 | public Integer getVoteIncrease() { 69 | return voteIncrease; 70 | } 71 | 72 | public void setVoteIncrease(Integer voteIncrease) { 73 | this.voteIncrease = voteIncrease; 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | StringBuilder sb = new StringBuilder(); 79 | sb.append(getClass().getSimpleName()); 80 | sb.append(" ["); 81 | sb.append("Hash = ").append(hashCode()); 82 | sb.append(", id=").append(id); 83 | sb.append(", ebookId=").append(ebookId); 84 | sb.append(", date=").append(date); 85 | sb.append(", viewCount=").append(viewCount); 86 | sb.append(", voteCount=").append(voteCount); 87 | sb.append(", viewIncrease=").append(viewIncrease); 88 | sb.append(", voteIncrease=").append(voteIncrease); 89 | sb.append("]"); 90 | return sb.toString(); 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %d{ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n 10 | 11 | 12 | 13 | 14 | ${PATH}/trace.log 15 | 16 | ${PATH}/trace.%d{yyyy-MM-dd}.%i.log 17 | 18 | 10MB 19 | 20 | 21 | 22 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n 23 | 24 | 25 | 26 | 27 | ${PATH}/error.log 28 | 29 | ${PATH}/error.%d{yyyy-MM-dd}.%i.log 30 | 31 | 10MB 32 | 33 | 34 | 35 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n 36 | 37 | 38 | ERROR 39 | ACCEPT 40 | DENY 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/resources/mapper/EbookSnapshotMapperCustom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | INSERT INTO ebook_snapshot(ebook_id, `date`, view_count, vote_count, view_increase, vote_increase) 18 | SELECT t1.id, curdate(), 0, 0, 0, 0 19 | FROM ebook t1 20 | WHERE NOT EXISTS(SELECT 1 21 | FROM ebook_snapshot t2 22 | WHERE t1.id = t2.ebook_id 23 | AND t2.`date` = curdate()); 24 | 25 | UPDATE ebook_snapshot t1, ebook t2 26 | SET t1.view_count = t2.view_count, 27 | t1.vote_count = t2.vote_count 28 | WHERE t1.`date` = curdate() 29 | AND t1.ebook_id = t2.id; 30 | 31 | UPDATE ebook_snapshot t1 LEFT JOIN (SELECT ebook_id, view_count, vote_count 32 | FROM ebook_snapshot 33 | WHERE `date` = date_sub(curdate(), INTERVAL 1 DAY)) t2 34 | ON t1.ebook_id = t2.ebook_id 35 | SET t1.view_increase = (t1.view_count - ifnull(t2.view_count, 0)), 36 | t1.vote_increase = (t1.vote_count - ifnull(t2.vote_count, 0)) 37 | WHERE t1.`date` = curdate(); 38 | 39 | 40 | 56 | 57 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/domain/Ebook.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.domain; 2 | 3 | public class Ebook { 4 | private Long id; 5 | 6 | private String name; 7 | 8 | private Long category1Id; 9 | 10 | private Long category2Id; 11 | 12 | private String description; 13 | 14 | private String cover; 15 | 16 | private Integer docCount; 17 | 18 | private Integer viewCount; 19 | 20 | private Integer voteCount; 21 | 22 | public Long getId() { 23 | return id; 24 | } 25 | 26 | public void setId(Long id) { 27 | this.id = id; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | 38 | public Long getCategory1Id() { 39 | return category1Id; 40 | } 41 | 42 | public void setCategory1Id(Long category1Id) { 43 | this.category1Id = category1Id; 44 | } 45 | 46 | public Long getCategory2Id() { 47 | return category2Id; 48 | } 49 | 50 | public void setCategory2Id(Long category2Id) { 51 | this.category2Id = category2Id; 52 | } 53 | 54 | public String getDescription() { 55 | return description; 56 | } 57 | 58 | public void setDescription(String description) { 59 | this.description = description; 60 | } 61 | 62 | public String getCover() { 63 | return cover; 64 | } 65 | 66 | public void setCover(String cover) { 67 | this.cover = cover; 68 | } 69 | 70 | public Integer getDocCount() { 71 | return docCount; 72 | } 73 | 74 | public void setDocCount(Integer docCount) { 75 | this.docCount = docCount; 76 | } 77 | 78 | public Integer getViewCount() { 79 | return viewCount; 80 | } 81 | 82 | public void setViewCount(Integer viewCount) { 83 | this.viewCount = viewCount; 84 | } 85 | 86 | public Integer getVoteCount() { 87 | return voteCount; 88 | } 89 | 90 | public void setVoteCount(Integer voteCount) { 91 | this.voteCount = voteCount; 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return getClass().getSimpleName() + 97 | " [" + 98 | "Hash = " + hashCode() + 99 | ", id=" + id + 100 | ", name=" + name + 101 | ", category1Id=" + category1Id + 102 | ", category2Id=" + category2Id + 103 | ", description=" + description + 104 | ", cover=" + cover + 105 | ", docCount=" + docCount + 106 | ", viewCount=" + viewCount + 107 | ", voteCount=" + voteCount + 108 | "]"; 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/interceptor/LoginInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.interceptor; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | import javax.annotation.Resource; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | 15 | /** 16 | * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 17 | */ 18 | @Component 19 | public class LoginInterceptor implements HandlerInterceptor { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class); 22 | 23 | @Resource 24 | private RedisTemplate redisTemplate; 25 | 26 | @Override 27 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 28 | // 打印请求信息 29 | LOG.info("------------- LoginInterceptor 开始 -------------"); 30 | long startTime = System.currentTimeMillis(); 31 | request.setAttribute("requestStartTime", startTime); 32 | 33 | // OPTIONS请求不做校验, 34 | // 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验 35 | if(request.getMethod().toUpperCase().equals("OPTIONS")){ 36 | return true; 37 | } 38 | 39 | String path = request.getRequestURL().toString(); 40 | LOG.info("接口登录拦截:,path:{}", path); 41 | 42 | //获取header的token参数 43 | String token = request.getHeader("token"); 44 | LOG.info("登录校验开始,token:{}", token); 45 | if (token == null || token.isEmpty()) { 46 | LOG.info( "token为空,请求被拦截" ); 47 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 48 | return false; 49 | } 50 | Object object = redisTemplate.opsForValue().get(token); 51 | if (object == null) { 52 | LOG.warn( "token无效,请求被拦截" ); 53 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 54 | return false; 55 | } else { 56 | LOG.info("已登录:{}", object); 57 | return true; 58 | } 59 | } 60 | 61 | @Override 62 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 63 | long startTime = (Long) request.getAttribute("requestStartTime"); 64 | LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime); 65 | } 66 | 67 | @Override 68 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 69 | // LOG.info("LogInterceptor 结束"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/service/EbookService.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.service; 2 | 3 | import com.github.pagehelper.PageHelper; 4 | import com.github.pagehelper.PageInfo; 5 | import io.github.yubincloud.fairywiki.domain.Ebook; 6 | import io.github.yubincloud.fairywiki.domain.EbookExample; 7 | import io.github.yubincloud.fairywiki.dto.req.EbookQueryReqDto; 8 | import io.github.yubincloud.fairywiki.dto.req.EbookSaveReqDto; 9 | import io.github.yubincloud.fairywiki.dto.resp.EbookQueryRespDto; 10 | import io.github.yubincloud.fairywiki.dto.resp.PageRespDto; 11 | import io.github.yubincloud.fairywiki.mapper.EbookMapper; 12 | import io.github.yubincloud.fairywiki.utils.CopyUtil; 13 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.util.ObjectUtils; 18 | 19 | import javax.annotation.Resource; 20 | import java.util.List; 21 | 22 | @Service 23 | public class EbookService { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(EbookService.class); 26 | 27 | @Resource 28 | private EbookMapper ebookMapper; 29 | 30 | @Resource 31 | private SnowFlake snowFlake; 32 | 33 | 34 | /** 35 | * 根据查询条件对数据库中的 ebook 进行查询并返回查询到的 ebook 36 | */ 37 | public PageRespDto queryEbooks(EbookQueryReqDto reqDto) { 38 | EbookExample ebookExample = new EbookExample(); 39 | EbookExample.Criteria criteria = ebookExample.createCriteria(); 40 | if (!ObjectUtils.isEmpty(reqDto.getName())) { 41 | criteria.andNameLike("%" + reqDto.getName() + "%"); 42 | } 43 | if (!ObjectUtils.isEmpty(reqDto.getCategoryId2())) { 44 | criteria.andCategory2IdEqualTo(reqDto.getCategoryId2()); 45 | } 46 | PageHelper.startPage(reqDto.getPageNum(), reqDto.getPageSize()); 47 | List ebookList = ebookMapper.selectByExample(ebookExample); 48 | 49 | PageInfo pageInfo = new PageInfo<>(ebookList); 50 | LOG.info("总行数:{}", pageInfo.getTotal()); 51 | LOG.info("总页数:{}", pageInfo.getPages()); 52 | 53 | // 列表复制 54 | List list = CopyUtil.copyList(ebookList, EbookQueryRespDto.class); 55 | 56 | PageRespDto pageRespDto = new PageRespDto<>(); 57 | pageRespDto.setTotal(pageInfo.getTotal()); 58 | pageRespDto.setList(list); 59 | 60 | return pageRespDto; 61 | } 62 | 63 | /** 64 | * 根据 EbookSaveReqDto 来保存一个 ebook 记录,若 id 为空则新增,不为空则更新 65 | */ 66 | public void save(EbookSaveReqDto reqDto) { 67 | Ebook ebookRecord = CopyUtil.copy(reqDto, Ebook.class); 68 | if (ObjectUtils.isEmpty(ebookRecord.getId())) { // 判断 id 是否为空 69 | ebookRecord.setId(snowFlake.nextId()); 70 | ebookMapper.insertSelective(ebookRecord); 71 | } else { 72 | ebookMapper.updateByPrimaryKey(ebookRecord); 73 | } 74 | } 75 | 76 | public void deleteOneEbook(Long ebookId) { 77 | ebookMapper.deleteByPrimaryKey(ebookId); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/controller/DocController.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.controller; 2 | 3 | import io.github.yubincloud.fairywiki.dto.req.DocDeleteReqDto; 4 | import io.github.yubincloud.fairywiki.dto.req.DocQueryReqDto; 5 | import io.github.yubincloud.fairywiki.dto.req.DocSaveReqDto; 6 | import io.github.yubincloud.fairywiki.dto.resp.DocQueryRespDto; 7 | import io.github.yubincloud.fairywiki.dto.resp.ErrorCode; 8 | import io.github.yubincloud.fairywiki.dto.resp.PageRespDto; 9 | import io.github.yubincloud.fairywiki.dto.resp.RestfulModel; 10 | import io.github.yubincloud.fairywiki.service.DocService; 11 | import io.swagger.annotations.Api; 12 | import io.swagger.annotations.ApiOperation; 13 | import io.swagger.annotations.ApiParam; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import javax.annotation.Resource; 17 | import javax.validation.Valid; 18 | import java.util.List; 19 | 20 | @Api("文档管理") 21 | @RestController 22 | @RequestMapping("/doc") 23 | public class DocController { 24 | 25 | @Resource 26 | private DocService docService; 27 | 28 | 29 | @GetMapping("/query/{ebookId}") 30 | @ApiOperation(value = "获取属于某个 ebook 的全部 doc ") 31 | public RestfulModel> queryDocs(@PathVariable Long ebookId) { 32 | List docList = docService.queryDocs(ebookId); 33 | return new RestfulModel<>(ErrorCode.SUCCESS, "", docList); 34 | } 35 | 36 | /** 37 | * 对 doc 进行查询的接口 38 | * @param docQueryReqDto 查询条件的参数 39 | * @return 查询到的所有doc 40 | */ 41 | @GetMapping("/query") 42 | public RestfulModel> queryDocs(@Valid DocQueryReqDto docQueryReqDto) { 43 | PageRespDto bookList = docService.queryDocs(docQueryReqDto); 44 | return new RestfulModel<>(ErrorCode.SUCCESS, "", bookList); 45 | } 46 | 47 | 48 | @PostMapping("/save") 49 | @ApiOperation(value = "保存一个 doc", 50 | notes = "若id非空则为更新,否则为新增") 51 | public RestfulModel saveDoc(@RequestBody @Valid DocSaveReqDto docSaveReqDto) { 52 | docService.save(docSaveReqDto); 53 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 54 | } 55 | 56 | @DeleteMapping("/delete") 57 | @ApiOperation(value = "删除一个 doc") 58 | public RestfulModel deleteDoc(@RequestBody @Valid DocDeleteReqDto docDeleteReqDto) { 59 | docService.deleteDocs(docDeleteReqDto.getIds()); 60 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 61 | } 62 | 63 | @GetMapping("/read-content/{docId}") 64 | @ApiOperation(value = "读取文档的内容", notes = "同时会对该文档的阅读数 + 1") 65 | @ApiParam(name = "docId", value = "文档的id", required = true) 66 | public RestfulModel readDocContent(@PathVariable Long docId) { 67 | String docContent = docService.readDocContent(docId); 68 | return new RestfulModel<>(0, "", docContent); 69 | } 70 | 71 | @GetMapping("/vote/{docId}") 72 | @ApiOperation(value = "为一个doc点赞") 73 | @ApiParam(name = "docId", value = "文档的id", required = true) 74 | public RestfulModel voteDoc(@PathVariable Long docId) { 75 | docService.vote(docId); 76 | return new RestfulModel<>(0, "", 0); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/service/CategoryService.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.service; 2 | 3 | import com.github.pagehelper.PageHelper; 4 | import com.github.pagehelper.PageInfo; 5 | import io.github.yubincloud.fairywiki.domain.Category; 6 | import io.github.yubincloud.fairywiki.domain.CategoryExample; 7 | import io.github.yubincloud.fairywiki.dto.req.CategoryQueryReqDto; 8 | import io.github.yubincloud.fairywiki.dto.req.CategorySaveReqDto; 9 | import io.github.yubincloud.fairywiki.dto.resp.CategoryQueryRespDto; 10 | import io.github.yubincloud.fairywiki.dto.resp.PageRespDto; 11 | import io.github.yubincloud.fairywiki.mapper.CategoryMapper; 12 | import io.github.yubincloud.fairywiki.utils.CopyUtil; 13 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.util.ObjectUtils; 18 | 19 | import javax.annotation.Resource; 20 | import java.util.List; 21 | 22 | @Service 23 | public class CategoryService { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(CategoryService.class); 26 | 27 | @Resource 28 | private CategoryMapper categoryMapper; 29 | 30 | @Resource 31 | private SnowFlake snowFlake; 32 | 33 | /** 34 | * 获取全部 Category 35 | */ 36 | public List fetchAllCategories() { 37 | CategoryExample categoryExample = new CategoryExample(); 38 | categoryExample.setOrderByClause("sort asc"); 39 | List categoryList = categoryMapper.selectByExample(categoryExample); 40 | return CopyUtil.copyList(categoryList, CategoryQueryRespDto.class); 41 | } 42 | 43 | 44 | /** 45 | * 根据查询条件对数据库中的 category 进行查询并返回查询到的 category 46 | */ 47 | public PageRespDto queryCategorys(CategoryQueryReqDto reqDto) { 48 | CategoryExample categoryExample = new CategoryExample(); 49 | CategoryExample.Criteria criteria = categoryExample.createCriteria(); 50 | PageHelper.startPage(reqDto.getPageNum(), reqDto.getPageSize()); 51 | List categoryList = categoryMapper.selectByExample(categoryExample); 52 | 53 | PageInfo pageInfo = new PageInfo<>(categoryList); 54 | LOG.info("总行数:{}", pageInfo.getTotal()); 55 | LOG.info("总页数:{}", pageInfo.getPages()); 56 | 57 | // 列表复制 58 | List list = CopyUtil.copyList(categoryList, CategoryQueryRespDto.class); 59 | 60 | PageRespDto pageRespDto = new PageRespDto<>(); 61 | pageRespDto.setTotal(pageInfo.getTotal()); 62 | pageRespDto.setList(list); 63 | 64 | return pageRespDto; 65 | } 66 | 67 | /** 68 | * 根据 CategorySaveReqDto 来保存一个 category 记录,若 id 为空则新增,不为空则更新 69 | */ 70 | public void save(CategorySaveReqDto reqDto) { 71 | Category categoryRecord = CopyUtil.copy(reqDto, Category.class); 72 | if (ObjectUtils.isEmpty(categoryRecord.getId())) { // 判断 id 是否为空 73 | categoryRecord.setId(snowFlake.nextId()); 74 | categoryMapper.insertSelective(categoryRecord); 75 | } else { 76 | categoryMapper.updateByPrimaryKey(categoryRecord); 77 | } 78 | } 79 | 80 | public void deleteOneCategory(Long categoryId) { 81 | categoryMapper.deleteByPrimaryKey(categoryId); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/utils/SnowFlake.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.utils; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.text.ParseException; 6 | 7 | /** 8 | * Twitter的分布式自增ID雪花算法 9 | **/ 10 | @Component 11 | public class SnowFlake { 12 | 13 | /** 14 | * 起始的时间戳 15 | */ 16 | private final static long START_STMP = 1609459200000L; // 2021-01-01 00:00:00 17 | 18 | /** 19 | * 每一部分占用的位数 20 | */ 21 | private final static long SEQUENCE_BIT = 12; //序列号占用的位数 22 | private final static long MACHINE_BIT = 5; //机器标识占用的位数 23 | private final static long DATACENTER_BIT = 5;//数据中心占用的位数 24 | 25 | /** 26 | * 每一部分的最大值 27 | */ 28 | private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); 29 | private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); 30 | private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); 31 | 32 | /** 33 | * 每一部分向左的位移 34 | */ 35 | private final static long MACHINE_LEFT = SEQUENCE_BIT; 36 | private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; 37 | private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; 38 | 39 | private long datacenterId = 1; //数据中心 40 | private long machineId = 1; //机器标识 41 | private long sequence = 0L; //序列号 42 | private long lastStmp = -1L;//上一次时间戳 43 | 44 | public SnowFlake() { 45 | } 46 | 47 | public SnowFlake(long datacenterId, long machineId) { 48 | if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { 49 | throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); 50 | } 51 | if (machineId > MAX_MACHINE_NUM || machineId < 0) { 52 | throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); 53 | } 54 | this.datacenterId = datacenterId; 55 | this.machineId = machineId; 56 | } 57 | 58 | /** 59 | * 产生下一个ID 60 | * 61 | * @return 62 | */ 63 | public synchronized long nextId() { 64 | long currStmp = getNewstmp(); 65 | if (currStmp < lastStmp) { 66 | throw new RuntimeException("Clock moved backwards. Refusing to generate id"); 67 | } 68 | 69 | if (currStmp == lastStmp) { 70 | //相同毫秒内,序列号自增 71 | sequence = (sequence + 1) & MAX_SEQUENCE; 72 | //同一毫秒的序列数已经达到最大 73 | if (sequence == 0L) { 74 | currStmp = getNextMill(); 75 | } 76 | } else { 77 | //不同毫秒内,序列号置为0 78 | sequence = 0L; 79 | } 80 | 81 | lastStmp = currStmp; 82 | 83 | return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分 84 | | datacenterId << DATACENTER_LEFT //数据中心部分 85 | | machineId << MACHINE_LEFT //机器标识部分 86 | | sequence; //序列号部分 87 | } 88 | 89 | private long getNextMill() { 90 | long mill = getNewstmp(); 91 | while (mill <= lastStmp) { 92 | mill = getNewstmp(); 93 | } 94 | return mill; 95 | } 96 | 97 | private long getNewstmp() { 98 | return System.currentTimeMillis(); 99 | } 100 | 101 | public static void main(String[] args) throws ParseException { 102 | // 时间戳 103 | // System.out.println(System.currentTimeMillis()); 104 | // System.out.println(new Date().getTime()); 105 | // 106 | // String dateTime = "2021-01-01 08:00:00"; 107 | // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 108 | // System.out.println(sdf.parse(dateTime).getTime()); 109 | 110 | SnowFlake snowFlake = new SnowFlake(1, 1); 111 | 112 | long start = System.currentTimeMillis(); 113 | for (int i = 0; i < 10; i++) { 114 | System.out.println(snowFlake.nextId()); 115 | System.out.println(System.currentTimeMillis() - start); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /web/src/components/the-header.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 134 | 135 | 152 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.controller; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import io.github.yubincloud.fairywiki.dto.req.UserLoginReqDto; 5 | import io.github.yubincloud.fairywiki.dto.req.UserQueryReqDto; 6 | import io.github.yubincloud.fairywiki.dto.req.UserResetPwdReqDto; 7 | import io.github.yubincloud.fairywiki.dto.req.UserSaveReqDto; 8 | import io.github.yubincloud.fairywiki.dto.resp.*; 9 | import io.github.yubincloud.fairywiki.service.UserService; 10 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 11 | import io.swagger.annotations.Api; 12 | import io.swagger.annotations.ApiOperation; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | import org.springframework.util.DigestUtils; 17 | import org.springframework.web.bind.annotation.*; 18 | import org.springframework.web.client.RestTemplate; 19 | 20 | import javax.annotation.Resource; 21 | import javax.validation.Valid; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | 25 | @Api("用户相关接口") 26 | @RestController 27 | @RequestMapping("/user") 28 | public class UserController { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(UserController.class); 31 | 32 | @Resource 33 | private UserService userService; 34 | 35 | @Resource 36 | private SnowFlake snowFlake; 37 | 38 | @Resource 39 | private RedisTemplate redisTemplate; 40 | 41 | @GetMapping("/list") 42 | public RestfulModel> list(@Valid UserQueryReqDto userQueryReqDto) { 43 | PageRespDto userList = userService.list(userQueryReqDto); 44 | return new RestfulModel<>(ErrorCode.SUCCESS, "", userList); 45 | } 46 | 47 | @PostMapping("/save") 48 | public RestfulModel saveUser(@RequestBody @Valid UserSaveReqDto userSaveReqDto) { 49 | userSaveReqDto.setPassword( 50 | DigestUtils.md5DigestAsHex(userSaveReqDto.getPassword().getBytes())); 51 | userService.save(userSaveReqDto); 52 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 53 | } 54 | 55 | @DeleteMapping("/delete/{userId}") 56 | public RestfulModel deleteUser(@PathVariable Long userId) { 57 | userService.delete(userId); 58 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 59 | } 60 | 61 | @PostMapping("/reset-pwd") 62 | public RestfulModel resetPwd(@RequestBody @Valid UserResetPwdReqDto userResetPwdReqDto) { 63 | userResetPwdReqDto.setPassword(DigestUtils.md5DigestAsHex(userResetPwdReqDto.getPassword().getBytes())); 64 | userService.resetPwd(userResetPwdReqDto); 65 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 66 | } 67 | 68 | /** 69 | * 用户登录接口 70 | * @param userLoginReqDto 用户的用户名及密码 71 | * @return 登录成功则返回该用户的信息,失败则返回登录失败提示 72 | */ 73 | @PostMapping("/login") 74 | public RestfulModel login(@RequestBody @Valid UserLoginReqDto userLoginReqDto) { 75 | userLoginReqDto.setPassword( 76 | DigestUtils.md5DigestAsHex(userLoginReqDto.getPassword().getBytes()) 77 | ); 78 | UserLoginRespDto userLoginRespDto = userService.login(userLoginReqDto); 79 | 80 | String token = Long.toString(snowFlake.nextId()); 81 | LOG.info("生成单点登录token:{},并放入redis中", token); 82 | userLoginRespDto.setToken(token); 83 | redisTemplate.opsForValue().set(token, JSONObject.toJSONString(userLoginRespDto), 3600 * 24, TimeUnit.SECONDS); 84 | return new RestfulModel<>(ErrorCode.SUCCESS, "", userLoginRespDto); 85 | } 86 | 87 | @RequestMapping("/redis/set/{key}/{value}") 88 | public String set(@PathVariable Long key, @PathVariable String value) { 89 | redisTemplate.opsForValue().set(Long.toString(key), value, 3600, TimeUnit.SECONDS); 90 | LOG.info("key: {}, value: {}", key, value); 91 | return "success"; 92 | } 93 | 94 | @RequestMapping("/redis/get/{key}") 95 | public Object get(@PathVariable String key) { 96 | Object object = redisTemplate.opsForValue().get(key); 97 | LOG.info("key: {}, value: {}", key, object); 98 | return object; 99 | } 100 | 101 | @ApiOperation(value = "退出登录", 102 | notes = "该接口会清除掉存放于 redis 中所传入的 token") 103 | @GetMapping("/logout/{userToken}") 104 | public RestfulModel logout(@PathVariable String userToken) { 105 | redisTemplate.delete(userToken); 106 | LOG.info("redis 中清除 token:{}", userToken); 107 | return new RestfulModel<>(ErrorCode.SUCCESS, "", 0); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/aspect/LogAspect.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.aspect; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.alibaba.fastjson.support.spring.PropertyPreFilters; 5 | import io.github.yubincloud.fairywiki.utils.RequestContext; 6 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 7 | import org.aspectj.lang.JoinPoint; 8 | import org.aspectj.lang.ProceedingJoinPoint; 9 | import org.aspectj.lang.Signature; 10 | import org.aspectj.lang.annotation.Around; 11 | import org.aspectj.lang.annotation.Aspect; 12 | import org.aspectj.lang.annotation.Before; 13 | import org.aspectj.lang.annotation.Pointcut; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.slf4j.MDC; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.web.context.request.RequestContextHolder; 19 | import org.springframework.web.context.request.ServletRequestAttributes; 20 | import org.springframework.web.multipart.MultipartFile; 21 | 22 | import javax.annotation.Resource; 23 | import javax.servlet.ServletRequest; 24 | import javax.servlet.ServletResponse; 25 | import javax.servlet.http.HttpServletRequest; 26 | 27 | @Aspect 28 | @Component 29 | public class LogAspect { 30 | 31 | private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class); 32 | 33 | @Resource 34 | private SnowFlake snowFlake; 35 | 36 | /** 定义一个切点 */ 37 | @Pointcut("execution(public * io.github.yubincloud.fairywiki.controller..*Controller.*(..))") 38 | public void controllerPointcut() {} 39 | 40 | 41 | @Before("controllerPointcut()") 42 | public void doBefore(JoinPoint joinPoint) { 43 | 44 | // 增加日志流水号 45 | MDC.put("LOG_ID", String.valueOf(snowFlake.nextId())); 46 | 47 | // 开始打印请求日志 48 | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 49 | HttpServletRequest request = attributes.getRequest(); 50 | Signature signature = joinPoint.getSignature(); 51 | String name = signature.getName(); 52 | 53 | // 打印请求信息 54 | LOG.info("------------- 开始 -------------"); 55 | LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod()); 56 | LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name); 57 | LOG.info("远程地址: {}", request.getRemoteAddr()); 58 | 59 | RequestContext.setRemoteAddr(getRemoteIp(request)); 60 | 61 | // 打印请求参数 62 | Object[] args = joinPoint.getArgs(); 63 | // LOG.info("请求参数: {}", JSONObject.toJSONString(args)); 64 | 65 | Object[] arguments = new Object[args.length]; 66 | for (int i = 0; i < args.length; i++) { 67 | if (args[i] instanceof ServletRequest 68 | || args[i] instanceof ServletResponse 69 | || args[i] instanceof MultipartFile) { 70 | continue; 71 | } 72 | arguments[i] = args[i]; 73 | } 74 | // 排除字段,敏感字段或太长的字段不显示 75 | String[] excludeProperties = {"password", "file"}; 76 | PropertyPreFilters filters = new PropertyPreFilters(); 77 | PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); 78 | excludefilter.addExcludes(excludeProperties); 79 | LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter)); 80 | } 81 | 82 | @Around("controllerPointcut()") 83 | public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 84 | long startTime = System.currentTimeMillis(); 85 | Object result = proceedingJoinPoint.proceed(); 86 | // 排除字段,敏感字段或太长的字段不显示 87 | String[] excludeProperties = {"password", "file"}; 88 | PropertyPreFilters filters = new PropertyPreFilters(); 89 | PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); 90 | excludefilter.addExcludes(excludeProperties); 91 | LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter)); 92 | LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime); 93 | return result; 94 | } 95 | 96 | /** 97 | * 使用nginx做反向代理,需要用该方法才能取到真实的远程IP 98 | */ 99 | public String getRemoteIp(HttpServletRequest request) { 100 | String ip = request.getHeader("x-forwarded-for"); 101 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 102 | ip = request.getHeader("Proxy-Client-IP"); 103 | } 104 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 105 | ip = request.getHeader("WL-Proxy-Client-IP"); 106 | } 107 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 108 | ip = request.getRemoteAddr(); 109 | } 110 | return ip; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /web/src/views/home.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 157 | 158 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import java.net.*; 18 | import java.io.*; 19 | import java.nio.channels.*; 20 | import java.util.Properties; 21 | 22 | public class MavenWrapperDownloader { 23 | 24 | private static final String WRAPPER_VERSION = "0.5.6"; 25 | /** 26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 27 | */ 28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 30 | 31 | /** 32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 33 | * use instead of the default one. 34 | */ 35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 36 | ".mvn/wrapper/maven-wrapper.properties"; 37 | 38 | /** 39 | * Path where the maven-wrapper.jar will be saved to. 40 | */ 41 | private static final String MAVEN_WRAPPER_JAR_PATH = 42 | ".mvn/wrapper/maven-wrapper.jar"; 43 | 44 | /** 45 | * Name of the property which should be used to override the default download url for the wrapper. 46 | */ 47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 48 | 49 | public static void main(String args[]) { 50 | System.out.println("- Downloader started"); 51 | File baseDirectory = new File(args[0]); 52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 53 | 54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 55 | // wrapperUrl parameter. 56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 57 | String url = DEFAULT_DOWNLOAD_URL; 58 | if (mavenWrapperPropertyFile.exists()) { 59 | FileInputStream mavenWrapperPropertyFileInputStream = null; 60 | try { 61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 62 | Properties mavenWrapperProperties = new Properties(); 63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 65 | } catch (IOException e) { 66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 67 | } finally { 68 | try { 69 | if (mavenWrapperPropertyFileInputStream != null) { 70 | mavenWrapperPropertyFileInputStream.close(); 71 | } 72 | } catch (IOException e) { 73 | // Ignore ... 74 | } 75 | } 76 | } 77 | System.out.println("- Downloading from: " + url); 78 | 79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 80 | if (!outputFile.getParentFile().exists()) { 81 | if (!outputFile.getParentFile().mkdirs()) { 82 | System.out.println( 83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 84 | } 85 | } 86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 87 | try { 88 | downloadFileFromURL(url, outputFile); 89 | System.out.println("Done"); 90 | System.exit(0); 91 | } catch (Throwable e) { 92 | System.out.println("- Error downloading"); 93 | e.printStackTrace(); 94 | System.exit(1); 95 | } 96 | } 97 | 98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 100 | String username = System.getenv("MVNW_USERNAME"); 101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 102 | Authenticator.setDefault(new Authenticator() { 103 | @Override 104 | protected PasswordAuthentication getPasswordAuthentication() { 105 | return new PasswordAuthentication(username, password); 106 | } 107 | }); 108 | } 109 | URL website = new URL(urlString); 110 | ReadableByteChannel rbc; 111 | rbc = Channels.newChannel(website.openStream()); 112 | FileOutputStream fos = new FileOutputStream(destination); 113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 114 | fos.close(); 115 | rbc.close(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/service/UserService.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.service; 2 | 3 | import com.github.pagehelper.PageHelper; 4 | import com.github.pagehelper.PageInfo; 5 | import io.github.yubincloud.fairywiki.domain.User; 6 | import io.github.yubincloud.fairywiki.domain.UserExample; 7 | import io.github.yubincloud.fairywiki.dto.req.UserLoginReqDto; 8 | import io.github.yubincloud.fairywiki.dto.req.UserResetPwdReqDto; 9 | import io.github.yubincloud.fairywiki.dto.resp.PageRespDto; 10 | import io.github.yubincloud.fairywiki.dto.resp.RestfulModel; 11 | import io.github.yubincloud.fairywiki.dto.resp.UserLoginRespDto; 12 | import io.github.yubincloud.fairywiki.exception.BusinessException; 13 | import io.github.yubincloud.fairywiki.exception.BusinessExceptionCode; 14 | import io.github.yubincloud.fairywiki.mapper.UserMapper; 15 | import io.github.yubincloud.fairywiki.dto.req.UserQueryReqDto; 16 | import io.github.yubincloud.fairywiki.dto.req.UserSaveReqDto; 17 | import io.github.yubincloud.fairywiki.dto.resp.UserQueryRespDto; 18 | import io.github.yubincloud.fairywiki.utils.CopyUtil; 19 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.stereotype.Service; 23 | import org.springframework.util.CollectionUtils; 24 | import org.springframework.util.ObjectUtils; 25 | 26 | import javax.annotation.Resource; 27 | import java.util.List; 28 | 29 | @Service 30 | public class UserService { 31 | 32 | private static final Logger LOG = LoggerFactory.getLogger(UserService.class); 33 | 34 | @Resource 35 | private UserMapper userMapper; 36 | 37 | @Resource 38 | private SnowFlake snowFlake; 39 | 40 | public PageRespDto list(UserQueryReqDto req) { 41 | UserExample userExample = new UserExample(); 42 | UserExample.Criteria criteria = userExample.createCriteria(); 43 | if (!ObjectUtils.isEmpty(req.getLoginName())) { 44 | criteria.andLoginNameEqualTo(req.getLoginName()); 45 | } 46 | PageHelper.startPage(req.getPageNum(), req.getPageSize()); 47 | List userList = userMapper.selectByExample(userExample); 48 | 49 | PageInfo pageInfo = new PageInfo<>(userList); 50 | LOG.info("总行数:{}", pageInfo.getTotal()); 51 | LOG.info("总页数:{}", pageInfo.getPages()); 52 | 53 | // 列表复制 54 | List list = CopyUtil.copyList(userList, UserQueryRespDto.class); 55 | 56 | PageRespDto pageRespDto = new PageRespDto<>(); 57 | pageRespDto.setTotal(pageInfo.getTotal()); 58 | pageRespDto.setList(list); 59 | 60 | return pageRespDto; 61 | } 62 | 63 | /** 64 | * 保存一个用户 65 | */ 66 | public void save(UserSaveReqDto req) { 67 | User user = CopyUtil.copy(req, User.class); 68 | if (ObjectUtils.isEmpty(req.getId())) { 69 | User userInDb = selectByLoginName(req.getLoginName()); // 存于数据库中的用户信息 70 | if (ObjectUtils.isEmpty(userInDb)) { // 新增之前判断一下 LoginName 是否重复 71 | // 新增 72 | user.setId(snowFlake.nextId()); 73 | userMapper.insert(user); 74 | } else { 75 | // 用户名已存在 76 | throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST); 77 | } 78 | } else { 79 | // 更新 80 | user.setLoginName(null); // 设置为空使得接下来对 user 的更新不再更新 LoginName 字段 81 | user.setPassword(null); 82 | userMapper.updateByPrimaryKeySelective(user); 83 | } 84 | } 85 | 86 | public void delete(Long id) { 87 | userMapper.deleteByPrimaryKey(id); 88 | } 89 | 90 | /** 91 | * 根据用户的 LoginName 来获取该用户的信息 92 | * @param LoginName 所要查询的用户的 LoginName 93 | * @return 含有该用户信息的 User 实体对象,若查询不到则返回 null 94 | */ 95 | public User selectByLoginName(String LoginName) { 96 | UserExample userExample = new UserExample(); 97 | UserExample.Criteria criteria = userExample.createCriteria(); 98 | criteria.andLoginNameEqualTo(LoginName); 99 | List userList = userMapper.selectByExample(userExample); 100 | if (CollectionUtils.isEmpty(userList)) { 101 | return null; 102 | } else { 103 | return userList.get(0); 104 | } 105 | } 106 | 107 | public void resetPwd(UserResetPwdReqDto reqDto) { 108 | User user = CopyUtil.copy(reqDto, User.class); 109 | userMapper.updateByPrimaryKeySelective(user); 110 | } 111 | 112 | /** 113 | * 对用户登录进行校验 114 | * @param reqDto 用户登录请求的信息 115 | * @return 若登录成功,则返回该用户的信息;失败则抛出 BusinessException 116 | */ 117 | public UserLoginRespDto login(UserLoginReqDto reqDto) { 118 | User userInDb = selectByLoginName(reqDto.getLoginName()); 119 | if (ObjectUtils.isEmpty(userInDb)) { 120 | // 用户名不存在 121 | LOG.info("用户名不存在,{}", reqDto.getLoginName()); 122 | throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR); 123 | } else { 124 | if (userInDb.getPassword().equals(reqDto.getPassword())) { 125 | // 登录成功 126 | UserLoginRespDto userLoginDto = CopyUtil.copy(userInDb, UserLoginRespDto.class); 127 | return userLoginDto; 128 | } else { 129 | // 密码不对 130 | LOG.info("密码不对,输入密码:{}, 数据库密码:{}", reqDto.getPassword(), userInDb.getPassword()); 131 | throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR); 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /sql-scripts/create_tables.sql: -------------------------------------------------------------------------------- 1 | # 电子书表 2 | drop table if exists `ebook`; 3 | create table `ebook` ( 4 | `id` bigint not null comment 'id', 5 | `name` varchar(50) comment '名称', 6 | `category1_id` bigint comment '分类1', 7 | `category2_id` bigint comment '分类2', 8 | `description` varchar(200) comment '描述', 9 | `cover` varchar(200) comment '封面', 10 | `doc_count` int not null default 0 comment '文档数', 11 | `view_count` int not null default 0 comment '阅读数', 12 | `vote_count` int not null default 0 comment '点赞数', 13 | primary key (`id`) 14 | ) engine=innodb default charset=utf8mb4 comment='电子书'; 15 | 16 | insert into `ebook` (id, name, description) values (1, 'Spring Boot 入门教程', '零基础入门 Java 开发,企业级应用开发最佳首选框架'); 17 | insert into `ebook` (id, name, description) values (2, 'Vue 入门教程', '零基础入门 Vue 开发,企业级应用开发最佳首选框架'); 18 | insert into `ebook` (id, name, description) values (3, 'Python 入门教程', '零基础入门 Python 开发,企业级应用开发最佳首选框架'); 19 | insert into `ebook` (id, name, description) values (4, 'Mysql 入门教程', '零基础入门 Mysql 开发,企业级应用开发最佳首选框架'); 20 | insert into `ebook` (id, name, description) values (5, 'Oracle 入门教程', '零基础入门 Oracle 开发,企业级应用开发最佳首选框架'); 21 | 22 | 23 | drop table if exists `category`; 24 | create table `category` ( 25 | `id` bigint not null comment 'id', 26 | `parent` bigint not null default 0 comment '父id', 27 | `name` varchar(255) not null comment '名称', 28 | `sort` int comment '顺序', 29 | primary key (`id`) 30 | ) engine=innodb default charset = utf8mb4 comment = '分类'; 31 | 32 | insert into `category` (id, parent, name, sort) values (100, 000, '前端开发', 100); 33 | insert into `category` (id, parent, name, sort) values (101, 100, 'Vue', 101); 34 | insert into `category` (id, parent, name, sort) values (102, 100, 'HTML & CSS', 102); 35 | insert into `category` (id, parent, name, sort) values (200, 000, 'Java', 200); 36 | insert into `category` (id, parent, name, sort) values (201, 200, '基础应用', 201); 37 | insert into `category` (id, parent, name, sort) values (202, 200, '框架应用', 202); 38 | insert into `category` (id, parent, name, sort) values (300, 000, 'Python', 300); 39 | insert into `category` (id, parent, name, sort) values (301, 300, '基础应用', 301); 40 | insert into `category` (id, parent, name, sort) values (302, 300, '进阶方向应用', 302); 41 | insert into `category` (id, parent, name, sort) values (400, 000, '数据库', 400); 42 | insert into `category` (id, parent, name, sort) values (401, 400, 'MySQL', 401); 43 | insert into `category` (id, parent, name, sort) values (500, 000, '其它', 500); 44 | insert into `category` (id, parent, name, sort) values (501, 500, '服务器', 501); 45 | insert into `category` (id, parent, name, sort) values (502, 500, '开发工具', 502); 46 | insert into `category` (id, parent, name, sort) values (503, 500, '热门服务端语言', 503); 47 | 48 | 49 | -- 文档表 50 | drop table if exists `doc`; 51 | create table `doc` ( 52 | `id` bigint not null comment 'id', 53 | `ebook_id` bigint not null default 0 comment '电子书id', 54 | `parent` bigint not null default 0 comment '父id', 55 | `name` varchar(50) not null comment '名称', 56 | `sort` int comment '顺序', 57 | `view_count` int default 0 comment '阅读数', 58 | `vote_count` int default 0 comment '点赞数', 59 | primary key (`id`) 60 | ) engine=innodb default charset=utf8mb4 comment='文档'; 61 | 62 | insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (1, 1, 0, '文档1', 1, 0, 0); 63 | insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (2, 1, 1, '文档1.1', 1, 0, 0); 64 | insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (3, 1, 0, '文档2', 2, 0, 0); 65 | insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (4, 1, 3, '文档2.1', 1, 0, 0); 66 | insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (5, 1, 3, '文档2.2', 2, 0, 0); 67 | insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (6, 1, 5, '文档2.2.1', 1, 0, 0); 68 | 69 | 70 | -- 文档内容 71 | drop table if exists `content`; 72 | create table `content` ( 73 | `id` bigint not null comment '文档id', 74 | `content` mediumtext not null comment '内容', 75 | primary key (`id`) 76 | ) engine=innodb default charset=utf8mb4 comment='文档内容'; 77 | 78 | -- 用户表 79 | drop table if exists `user`; 80 | create table `user` ( 81 | `id` bigint not null comment 'ID', 82 | `login_name` varchar(50) not null comment '登陆名', 83 | `name` varchar(50) comment '昵称', 84 | `password` char(32) not null comment '密码', 85 | primary key (`id`), 86 | unique key `login_name_unique` (`login_name`) 87 | ) engine=innodb default charset=utf8mb4 comment='用户'; 88 | 89 | insert into `user` (id, `login_name`, `name`, `password`) values (1, 'test', '测试', 'e70e2222a9d67c4f2eae107533359aa4'); 90 | 91 | -- 电子书快照表 92 | drop table if exists `ebook_snapshot`; 93 | create table `ebook_snapshot` ( 94 | `id` bigint auto_increment not null comment 'id', 95 | `ebook_id` bigint not null default 0 comment '电子书id', 96 | `date` date not null comment '快照日期', 97 | `view_count` int not null default 0 comment '阅读数', 98 | `vote_count` int not null default 0 comment '点赞数', 99 | `view_increase` int not null default 0 comment '阅读增长', 100 | `vote_increase` int not null default 0 comment '点赞增长', 101 | primary key (`id`), 102 | unique key `ebook_id_date_unique` (`ebook_id`, `date`) 103 | ) engine=innodb default charset=utf8mb4 comment='电子书快照表'; -------------------------------------------------------------------------------- /web/src/views/doc.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 146 | 147 | -------------------------------------------------------------------------------- /src/main/resources/mapper/DemoMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | and ${criterion.condition} 17 | 18 | 19 | and ${criterion.condition} #{criterion.value} 20 | 21 | 22 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 23 | 24 | 25 | and ${criterion.condition} 26 | 27 | #{listItem} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | and ${criterion.condition} 46 | 47 | 48 | and ${criterion.condition} #{criterion.value} 49 | 50 | 51 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 52 | 53 | 54 | and ${criterion.condition} 55 | 56 | #{listItem} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | `id`, `name` 68 | 69 | 83 | 89 | 90 | delete from demo 91 | where id = #{id,jdbcType=INTEGER} 92 | 93 | 94 | delete from demo 95 | 96 | 97 | 98 | 99 | 100 | insert into demo (id, `name`) 101 | values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}) 102 | 103 | 104 | insert into demo 105 | 106 | 107 | id, 108 | 109 | 110 | `name`, 111 | 112 | 113 | 114 | 115 | #{id,jdbcType=INTEGER}, 116 | 117 | 118 | #{name,jdbcType=VARCHAR}, 119 | 120 | 121 | 122 | 128 | 129 | update demo 130 | 131 | 132 | id = #{record.id,jdbcType=INTEGER}, 133 | 134 | 135 | `name` = #{record.name,jdbcType=VARCHAR}, 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | update demo 144 | set id = #{record.id,jdbcType=INTEGER}, 145 | `name` = #{record.name,jdbcType=VARCHAR} 146 | 147 | 148 | 149 | 150 | 151 | update demo 152 | 153 | 154 | `name` = #{name,jdbcType=VARCHAR}, 155 | 156 | 157 | where id = #{id,jdbcType=INTEGER} 158 | 159 | 160 | update demo 161 | set `name` = #{name,jdbcType=VARCHAR} 162 | where id = #{id,jdbcType=INTEGER} 163 | 164 | -------------------------------------------------------------------------------- /src/main/java/io/github/yubincloud/fairywiki/service/DocService.java: -------------------------------------------------------------------------------- 1 | package io.github.yubincloud.fairywiki.service; 2 | 3 | import com.github.pagehelper.PageHelper; 4 | import com.github.pagehelper.PageInfo; 5 | import io.github.yubincloud.fairywiki.domain.Content; 6 | import io.github.yubincloud.fairywiki.domain.Doc; 7 | import io.github.yubincloud.fairywiki.domain.DocExample; 8 | import io.github.yubincloud.fairywiki.dto.req.DocQueryReqDto; 9 | import io.github.yubincloud.fairywiki.dto.req.DocSaveReqDto; 10 | import io.github.yubincloud.fairywiki.dto.resp.DocQueryRespDto; 11 | import io.github.yubincloud.fairywiki.dto.resp.PageRespDto; 12 | import io.github.yubincloud.fairywiki.exception.BusinessException; 13 | import io.github.yubincloud.fairywiki.exception.BusinessExceptionCode; 14 | import io.github.yubincloud.fairywiki.mapper.ContentMapper; 15 | import io.github.yubincloud.fairywiki.mapper.DocMapper; 16 | import io.github.yubincloud.fairywiki.mapper.DocMapperCustom; 17 | import io.github.yubincloud.fairywiki.utils.CopyUtil; 18 | import io.github.yubincloud.fairywiki.utils.RedisUtil; 19 | import io.github.yubincloud.fairywiki.utils.RequestContext; 20 | import io.github.yubincloud.fairywiki.utils.SnowFlake; 21 | import org.apache.rocketmq.spring.core.RocketMQTemplate; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.slf4j.MDC; 25 | import org.springframework.stereotype.Service; 26 | import org.springframework.transaction.annotation.Transactional; 27 | import org.springframework.util.ObjectUtils; 28 | 29 | import javax.annotation.Resource; 30 | import java.util.List; 31 | 32 | @Service 33 | public class DocService { 34 | 35 | private static final Logger LOG = LoggerFactory.getLogger(DocService.class); 36 | 37 | @Resource 38 | private DocMapper docMapper; 39 | 40 | @Resource 41 | private ContentMapper contentMapper; 42 | 43 | @Resource 44 | private DocMapperCustom docMapperCustom; 45 | 46 | @Resource 47 | private SnowFlake snowFlake; 48 | 49 | @Resource 50 | private RedisUtil redisUtil; 51 | 52 | @Resource 53 | private WsService wsService; 54 | 55 | @Resource 56 | private RocketMQTemplate rocketMQTemplate; 57 | 58 | /** 59 | * 获取全部 Doc 60 | */ 61 | public List queryDocs(Long ebookId) { 62 | DocExample docExample = new DocExample(); 63 | docExample.createCriteria().andEbookIdEqualTo(ebookId); 64 | docExample.setOrderByClause("sort asc"); 65 | List docList = docMapper.selectByExample(docExample); 66 | return CopyUtil.copyList(docList, DocQueryRespDto.class); 67 | } 68 | 69 | 70 | /** 71 | * 根据查询条件对数据库中的 doc 进行查询并返回查询到的 doc 72 | */ 73 | public PageRespDto queryDocs(DocQueryReqDto reqDto) { 74 | DocExample docExample = new DocExample(); 75 | DocExample.Criteria criteria = docExample.createCriteria(); 76 | PageHelper.startPage(reqDto.getPageNum(), reqDto.getPageSize()); 77 | List docList = docMapper.selectByExample(docExample); 78 | 79 | PageInfo pageInfo = new PageInfo<>(docList); 80 | LOG.info("总行数:{}", pageInfo.getTotal()); 81 | LOG.info("总页数:{}", pageInfo.getPages()); 82 | 83 | // 列表复制 84 | List list = CopyUtil.copyList(docList, DocQueryRespDto.class); 85 | 86 | PageRespDto pageRespDto = new PageRespDto<>(); 87 | pageRespDto.setTotal(pageInfo.getTotal()); 88 | pageRespDto.setList(list); 89 | 90 | return pageRespDto; 91 | } 92 | 93 | /** 94 | * 根据 DocSaveReqDto 来保存一个 doc 记录,若 id 为空则新增,不为空则更新 95 | */ 96 | @Transactional 97 | public void save(DocSaveReqDto reqDto) { 98 | Doc docRecord = CopyUtil.copy(reqDto, Doc.class); 99 | Content docContent = CopyUtil.copy(reqDto, Content.class); 100 | if (ObjectUtils.isEmpty(docRecord.getId())) { // 判断 id 是否为空 101 | // 新增 102 | Long docId = snowFlake.nextId(); 103 | docRecord.setId(docId); 104 | docRecord.setViewCount(0); 105 | docRecord.setVoteCount(0); 106 | docMapper.insertSelective(docRecord); 107 | 108 | docContent.setId(docId); 109 | contentMapper.insertSelective(docContent); 110 | } else { 111 | // 更新 112 | docMapper.updateByPrimaryKey(docRecord); 113 | int count = contentMapper.updateByPrimaryKeyWithBLOBs(docContent); // 带大字段的更新 114 | if (count == 0) { 115 | contentMapper.insert(docContent); 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * 根据给定的文档 id 列表删除所有的文档 122 | * @param idList 由所要删除的文档 id 组成的列表 123 | */ 124 | public void deleteDocs(List idList) { 125 | DocExample docExample = new DocExample(); 126 | DocExample.Criteria criteria = docExample.createCriteria(); 127 | criteria.andIdIn(idList); 128 | docMapper.deleteByExample(docExample); 129 | } 130 | 131 | /** 132 | * 读取一篇文档的内容 133 | * @param docId 所要读取的文档的id 134 | * @return 该文档的内容 135 | */ 136 | public String readDocContent(Long docId) { 137 | Content content = contentMapper.selectByPrimaryKey(docId); 138 | docMapperCustom.increaseViewCount(docId); // 文档阅读数 + 1 139 | if (ObjectUtils.isEmpty(content)) 140 | return ""; 141 | return content.getContent(); 142 | } 143 | 144 | /** 145 | * 为某一个文档进行点赞 146 | * @param docId 文档的 id 147 | */ 148 | public void vote(Long docId) { 149 | String ip = RequestContext.getRemoteAddr(); 150 | String ipKey = constructIpKeyInRedis(docId, ip); 151 | if (redisUtil.validateRepeatedKey(ipKey, 5000)) { 152 | docMapperCustom.increaseVoteCount(docId); 153 | } else { 154 | throw new BusinessException(BusinessExceptionCode.VOTE_REPEAT); 155 | } 156 | // 向 ws 推送消息 157 | Doc docInDb = docMapper.selectByPrimaryKey(docId); 158 | String logId = MDC.get("LOG_ID"); 159 | // wsService.sendInfo("【" + docInDb.getName() + "】被点赞!", logId); 160 | rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docInDb.getName() + "】被点赞!"); 161 | } 162 | 163 | /** 164 | * 构造 IP + docID 作为存放于 redis 中的 key,24小时内不能重复 165 | * @return 构造生成的 key 166 | */ 167 | private String constructIpKeyInRedis(Long docId, String ip) { 168 | return "ODC_VOTE_" + docId + "_" + ip; 169 | } 170 | 171 | /** 172 | * 更新所有 Ebook 的阅读量、点赞量信息 173 | */ 174 | public void updateEbookFooter() { 175 | docMapperCustom.updateEbookFooter(); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | io.github.yubincloud 6 | fairy-wiki 7 | 0.0.1-SNAPSHOT 8 | fairy-wiki 9 | Demo project for Spring Boot 10 | 11 | 12 | 1.8 13 | UTF-8 14 | UTF-8 15 | 2.4.0 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | 25 | org.projectlombok 26 | lombok 27 | true 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | test 34 | 35 | 36 | org.junit.vintage 37 | junit-vintage-engine 38 | 39 | 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-devtools 45 | 46 | 47 | 48 | 49 | mysql 50 | mysql-connector-java 51 | 52 | 53 | 54 | 55 | org.mybatis.spring.boot 56 | mybatis-spring-boot-starter 57 | 2.1.4 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-aop 63 | 64 | 65 | 66 | com.alibaba 67 | fastjson 68 | 1.2.70 69 | 70 | 71 | 72 | com.github.pagehelper 73 | pagehelper-spring-boot-starter 74 | 1.2.13 75 | 76 | 77 | 78 | org.springframework.boot 79 | spring-boot-starter-validation 80 | 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-starter-data-redis 85 | 86 | 87 | 88 | io.springfox 89 | springfox-boot-starter 90 | 3.0.0 91 | 92 | 93 | 94 | com.github.xiaoymin 95 | knife4j-spring-boot-starter 96 | 3.0.2 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-starter-websocket 102 | 103 | 104 | 105 | org.apache.rocketmq 106 | rocketmq-spring-boot-starter 107 | 2.0.2 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.springframework.boot 116 | spring-boot-dependencies 117 | ${spring-boot.version} 118 | pom 119 | import 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-compiler-plugin 129 | 3.8.1 130 | 131 | 1.8 132 | 1.8 133 | UTF-8 134 | 135 | 136 | 137 | 138 | org.springframework.boot 139 | spring-boot-maven-plugin 140 | 2.3.7.RELEASE 141 | 142 | io.github.yubincloud.fairywiki.FairyWikiApplication 143 | 144 | 145 | 146 | repackage 147 | 148 | repackage 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | org.mybatis.generator 157 | mybatis-generator-maven-plugin 158 | 1.4.0 159 | 160 | src/main/resources/generator/generator-config.xml 161 | true 162 | true 163 | 164 | 165 | 166 | mysql 167 | mysql-connector-java 168 | 8.0.22 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /web/src/views/admin/admin-category.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 250 | 251 | 257 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /src/main/resources/mapper/ContentMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | and ${criterion.condition} 19 | 20 | 21 | and ${criterion.condition} #{criterion.value} 22 | 23 | 24 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 25 | 26 | 27 | and ${criterion.condition} 28 | 29 | #{listItem} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | and ${criterion.condition} 48 | 49 | 50 | and ${criterion.condition} #{criterion.value} 51 | 52 | 53 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 54 | 55 | 56 | and ${criterion.condition} 57 | 58 | #{listItem} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | id 70 | 71 | 72 | content 73 | 74 | 90 | 104 | 112 | 113 | delete from content 114 | where id = #{id,jdbcType=BIGINT} 115 | 116 | 117 | delete from content 118 | 119 | 120 | 121 | 122 | 123 | insert into content (id, content) 124 | values (#{id,jdbcType=BIGINT}, #{content,jdbcType=LONGVARCHAR}) 125 | 126 | 127 | insert into content 128 | 129 | 130 | id, 131 | 132 | 133 | content, 134 | 135 | 136 | 137 | 138 | #{id,jdbcType=BIGINT}, 139 | 140 | 141 | #{content,jdbcType=LONGVARCHAR}, 142 | 143 | 144 | 145 | 151 | 152 | update content 153 | 154 | 155 | id = #{record.id,jdbcType=BIGINT}, 156 | 157 | 158 | content = #{record.content,jdbcType=LONGVARCHAR}, 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | update content 167 | set id = #{record.id,jdbcType=BIGINT}, 168 | content = #{record.content,jdbcType=LONGVARCHAR} 169 | 170 | 171 | 172 | 173 | 174 | update content 175 | set id = #{record.id,jdbcType=BIGINT} 176 | 177 | 178 | 179 | 180 | 181 | update content 182 | 183 | 184 | content = #{content,jdbcType=LONGVARCHAR}, 185 | 186 | 187 | where id = #{id,jdbcType=BIGINT} 188 | 189 | 190 | update content 191 | set content = #{content,jdbcType=LONGVARCHAR} 192 | where id = #{id,jdbcType=BIGINT} 193 | 194 | -------------------------------------------------------------------------------- /src/main/resources/mapper/CategoryMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | and ${criterion.condition} 19 | 20 | 21 | and ${criterion.condition} #{criterion.value} 22 | 23 | 24 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 25 | 26 | 27 | and ${criterion.condition} 28 | 29 | #{listItem} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | and ${criterion.condition} 48 | 49 | 50 | and ${criterion.condition} #{criterion.value} 51 | 52 | 53 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 54 | 55 | 56 | and ${criterion.condition} 57 | 58 | #{listItem} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | id, parent, `name`, sort 70 | 71 | 85 | 91 | 92 | delete from category 93 | where id = #{id,jdbcType=BIGINT} 94 | 95 | 96 | delete from category 97 | 98 | 99 | 100 | 101 | 102 | insert into category (id, parent, `name`, 103 | sort) 104 | values (#{id,jdbcType=BIGINT}, #{parent,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, 105 | #{sort,jdbcType=INTEGER}) 106 | 107 | 108 | insert into category 109 | 110 | 111 | id, 112 | 113 | 114 | parent, 115 | 116 | 117 | `name`, 118 | 119 | 120 | sort, 121 | 122 | 123 | 124 | 125 | #{id,jdbcType=BIGINT}, 126 | 127 | 128 | #{parent,jdbcType=BIGINT}, 129 | 130 | 131 | #{name,jdbcType=VARCHAR}, 132 | 133 | 134 | #{sort,jdbcType=INTEGER}, 135 | 136 | 137 | 138 | 144 | 145 | update category 146 | 147 | 148 | id = #{record.id,jdbcType=BIGINT}, 149 | 150 | 151 | parent = #{record.parent,jdbcType=BIGINT}, 152 | 153 | 154 | `name` = #{record.name,jdbcType=VARCHAR}, 155 | 156 | 157 | sort = #{record.sort,jdbcType=INTEGER}, 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | update category 166 | set id = #{record.id,jdbcType=BIGINT}, 167 | parent = #{record.parent,jdbcType=BIGINT}, 168 | `name` = #{record.name,jdbcType=VARCHAR}, 169 | sort = #{record.sort,jdbcType=INTEGER} 170 | 171 | 172 | 173 | 174 | 175 | update category 176 | 177 | 178 | parent = #{parent,jdbcType=BIGINT}, 179 | 180 | 181 | `name` = #{name,jdbcType=VARCHAR}, 182 | 183 | 184 | sort = #{sort,jdbcType=INTEGER}, 185 | 186 | 187 | where id = #{id,jdbcType=BIGINT} 188 | 189 | 190 | update category 191 | set parent = #{parent,jdbcType=BIGINT}, 192 | `name` = #{name,jdbcType=VARCHAR}, 193 | sort = #{sort,jdbcType=INTEGER} 194 | where id = #{id,jdbcType=BIGINT} 195 | 196 | -------------------------------------------------------------------------------- /src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | and ${criterion.condition} 19 | 20 | 21 | and ${criterion.condition} #{criterion.value} 22 | 23 | 24 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 25 | 26 | 27 | and ${criterion.condition} 28 | 29 | #{listItem} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | and ${criterion.condition} 48 | 49 | 50 | and ${criterion.condition} #{criterion.value} 51 | 52 | 53 | and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} 54 | 55 | 56 | and ${criterion.condition} 57 | 58 | #{listItem} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | id, login_name, `name`, `password` 70 | 71 | 85 | 91 | 92 | delete from user 93 | where id = #{id,jdbcType=BIGINT} 94 | 95 | 96 | delete from user 97 | 98 | 99 | 100 | 101 | 102 | insert into user (id, login_name, `name`, 103 | `password`) 104 | values (#{id,jdbcType=BIGINT}, #{loginName,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, 105 | #{password,jdbcType=CHAR}) 106 | 107 | 108 | insert into user 109 | 110 | 111 | id, 112 | 113 | 114 | login_name, 115 | 116 | 117 | `name`, 118 | 119 | 120 | `password`, 121 | 122 | 123 | 124 | 125 | #{id,jdbcType=BIGINT}, 126 | 127 | 128 | #{loginName,jdbcType=VARCHAR}, 129 | 130 | 131 | #{name,jdbcType=VARCHAR}, 132 | 133 | 134 | #{password,jdbcType=CHAR}, 135 | 136 | 137 | 138 | 144 | 145 | update user 146 | 147 | 148 | id = #{record.id,jdbcType=BIGINT}, 149 | 150 | 151 | login_name = #{record.loginName,jdbcType=VARCHAR}, 152 | 153 | 154 | `name` = #{record.name,jdbcType=VARCHAR}, 155 | 156 | 157 | `password` = #{record.password,jdbcType=CHAR}, 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | update user 166 | set id = #{record.id,jdbcType=BIGINT}, 167 | login_name = #{record.loginName,jdbcType=VARCHAR}, 168 | `name` = #{record.name,jdbcType=VARCHAR}, 169 | `password` = #{record.password,jdbcType=CHAR} 170 | 171 | 172 | 173 | 174 | 175 | update user 176 | 177 | 178 | login_name = #{loginName,jdbcType=VARCHAR}, 179 | 180 | 181 | `name` = #{name,jdbcType=VARCHAR}, 182 | 183 | 184 | `password` = #{password,jdbcType=CHAR}, 185 | 186 | 187 | where id = #{id,jdbcType=BIGINT} 188 | 189 | 190 | update user 191 | set login_name = #{loginName,jdbcType=VARCHAR}, 192 | `name` = #{name,jdbcType=VARCHAR}, 193 | `password` = #{password,jdbcType=CHAR} 194 | where id = #{id,jdbcType=BIGINT} 195 | 196 | --------------------------------------------------------------------------------