├── .idea ├── .gitignore ├── compiler.xml ├── encodings.xml ├── flip.iml ├── jarRepositories.xml ├── misc.xml ├── modules.xml ├── uiDesigner.xml └── vcs.xml ├── Flip-back ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── flip │ │ ├── FlipBackApplication.java │ │ ├── annotation │ │ └── LimitRequest.java │ │ ├── aspect │ │ └── LimitRequestAspect.java │ │ ├── common │ │ └── Response.java │ │ ├── config │ │ ├── CorsConfig.java │ │ ├── ElasticConfig.java │ │ ├── MybatisPlusConfig.java │ │ ├── ObjectMapperConfig.java │ │ ├── RedisConfig.java │ │ ├── SecurityConfig.java │ │ └── WebConfig.java │ │ ├── controller │ │ ├── AccountController.java │ │ ├── CommentController.java │ │ ├── PostController.java │ │ ├── SearchController.java │ │ ├── SensitiveController.java │ │ ├── StatusController.java │ │ ├── TagController.java │ │ ├── TestController.java │ │ ├── UserController.java │ │ └── UserSysController.java │ │ ├── domain │ │ ├── dto │ │ │ └── LoggedUser.java │ │ ├── entity │ │ │ ├── Authority.java │ │ │ ├── BannedHistory.java │ │ │ ├── BannedUser.java │ │ │ ├── Comment.java │ │ │ ├── Post.java │ │ │ ├── PostTag.java │ │ │ ├── Role.java │ │ │ ├── SensitiveWord.java │ │ │ ├── Tag.java │ │ │ ├── TagOption.java │ │ │ └── User.java │ │ └── enums │ │ │ └── ResponseCode.java │ │ ├── exception │ │ └── InvalidTokenException.java │ │ ├── filter │ │ └── JwtAuthenticationFilter.java │ │ ├── handler │ │ ├── AutoFillHandler.java │ │ ├── GlobalExceptionHandler.java │ │ ├── LogoutHandler.java │ │ ├── LogoutSuccessHandlerImpl.java │ │ ├── NoLoginHandler.java │ │ └── NoPermissionHandler.java │ │ ├── mapper │ │ ├── AuthorityMapper.java │ │ ├── BannedUserMapper.java │ │ ├── CommentMapper.java │ │ ├── PostMapper.java │ │ ├── PostTagMapper.java │ │ ├── RoleMapper.java │ │ ├── SensitiveWordMapper.java │ │ ├── TagMapper.java │ │ ├── TagOptionMapper.java │ │ └── UserMapper.java │ │ ├── service │ │ ├── AccountService.java │ │ ├── BannedUserService.java │ │ ├── CommentService.java │ │ ├── PostService.java │ │ ├── SearchService.java │ │ ├── SensitiveWordService.java │ │ ├── TagOptionService.java │ │ ├── TagService.java │ │ ├── UserService.java │ │ └── impl │ │ │ ├── AccountServiceImpl.java │ │ │ ├── BannedUserServiceImpl.java │ │ │ ├── CommentServiceImpl.java │ │ │ ├── PostServiceImpl.java │ │ │ ├── SearchServiceImpl.java │ │ │ ├── SensitiveWordServiceImpl.java │ │ │ ├── TagOptionServiceImpl.java │ │ │ ├── TagServiceImpl.java │ │ │ ├── UserDetailServiceImpl.java │ │ │ └── UserServiceImpl.java │ │ ├── utils │ │ ├── AddressUtils.java │ │ ├── AvatarUtils.java │ │ ├── IpUtils.java │ │ ├── JwtUtils.java │ │ ├── LoggedUserUtils.java │ │ ├── PageUtils.java │ │ ├── RedisKeyUtils.java │ │ ├── SensitiveWordUtils.java │ │ ├── SystemUtils.java │ │ ├── TimeUtils.java │ │ └── elastic │ │ │ ├── ElasticPostUtils.java │ │ │ ├── ElasticUserUtils.java │ │ │ └── ElasticUtils.java │ │ └── validation │ │ └── VG.java │ └── resources │ ├── META-INF │ └── MANIFEST.MF │ ├── application-dev.yaml │ ├── application-prod.yaml │ ├── application.yaml │ ├── mapper │ ├── AuthorityMapper.xml │ ├── BannedUserMapper.xml │ ├── CommentMapper.xml │ ├── PostMapper.xml │ ├── RoleMapper.xml │ ├── TagMapper.xml │ └── UserMapper.xml │ └── templates │ └── mail-register-template.html ├── Flip-front ├── .gitignore ├── Flip-front.iml ├── env │ ├── .env │ ├── .env.dev │ └── .env.prod ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── assets │ │ └── strawberry │ │ │ ├── fonts │ │ │ ├── StrawberryIcon-Free.eot │ │ │ ├── StrawberryIcon-Free.svg │ │ │ ├── StrawberryIcon-Free.ttf │ │ │ └── StrawberryIcon-Free.woff │ │ │ ├── selection.json │ │ │ ├── style.css │ │ │ ├── style.scss │ │ │ └── variables.scss │ ├── favicon.ico │ ├── html │ │ └── ie.html │ └── images │ │ ├── admin_panel.jpg │ │ ├── avatar_change.jpg │ │ ├── comment_drawer.jpg │ │ ├── login.jpg │ │ ├── main.jpg │ │ ├── post.jpg │ │ ├── profile.jpg │ │ ├── replies.jpg │ │ ├── replies_black.jpg │ │ ├── search.jpg │ │ ├── search_black.jpg │ │ ├── tag.jpg │ │ ├── tags.jpg │ │ ├── tags_black.jpg │ │ └── write.jpg ├── src │ ├── App.vue │ ├── api │ │ ├── admin │ │ │ ├── overviewAPI.js │ │ │ ├── sensitiveAPI.js │ │ │ ├── tagAPI.js │ │ │ └── userSysAPI.js │ │ ├── commentAPI.js │ │ ├── loginAPI.js │ │ ├── postAPI.js │ │ ├── searchAPI.js │ │ ├── testAPI.js │ │ ├── uploadAPI.js │ │ └── userAPI.js │ ├── assets │ │ ├── emoji.ts │ │ └── styles │ │ │ └── dark │ │ │ └── css-vars.css │ ├── components │ │ ├── comment │ │ │ ├── Comments.vue │ │ │ ├── make │ │ │ │ └── MakeComment.vue │ │ │ └── replies │ │ │ │ └── ShowReplies.vue │ │ ├── cropper │ │ │ └── AvatarCropper.vue │ │ ├── editor │ │ │ └── Editor.vue │ │ ├── layout │ │ │ ├── aside │ │ │ │ ├── index │ │ │ │ │ ├── CountdownAside.vue │ │ │ │ │ ├── HotTagAside.vue │ │ │ │ │ └── StatisticsAside.vue │ │ │ │ └── post │ │ │ │ │ └── PostAuthorAside.vue │ │ │ ├── dialog │ │ │ │ ├── DevelopingDialog.vue │ │ │ │ ├── NoLoginDialog.vue │ │ │ │ └── NoVerifyEmailDialog.vue │ │ │ ├── footer │ │ │ │ └── Footer.vue │ │ │ └── header │ │ │ │ ├── Header.vue │ │ │ │ ├── expand │ │ │ │ └── LeftExpandMenu.vue │ │ │ │ ├── search │ │ │ │ └── SearchBar.vue │ │ │ │ └── togger │ │ │ │ └── ThemeToggle.vue │ │ ├── page │ │ │ └── CascadePage.vue │ │ ├── post │ │ │ ├── Operations.vue │ │ │ └── PostList.vue │ │ ├── search │ │ │ ├── SearchedPosts.vue │ │ │ └── SearchedUsers.vue │ │ └── user │ │ │ ├── BanUserDialog.vue │ │ │ ├── profile │ │ │ ├── UserBookmarks.vue │ │ │ ├── UserComments.vue │ │ │ ├── UserFollows.vue │ │ │ └── UserPosts.vue │ │ │ └── setting │ │ │ ├── UpdateAccount.vue │ │ │ ├── UpdateAvatar.vue │ │ │ └── UpdateProfile.vue │ ├── directive │ │ ├── authority │ │ │ ├── hasAuthority.js │ │ │ └── hasRole.js │ │ └── index.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── stores │ │ ├── tabStore.js │ │ ├── themeStore.js │ │ └── userStore.js │ ├── utils │ │ ├── errorMsg.js │ │ ├── permission.js │ │ ├── request.js │ │ ├── tags.js │ │ └── token.js │ └── views │ │ ├── Index.vue │ │ ├── about │ │ └── About.vue │ │ ├── account │ │ ├── Activate.vue │ │ ├── Login.vue │ │ └── Register.vue │ │ ├── admin │ │ └── Admin.vue │ │ ├── error │ │ └── 404.vue │ │ ├── main │ │ └── Main.vue │ │ ├── post │ │ ├── view │ │ │ └── ViewPost.vue │ │ └── write │ │ │ └── WritePost.vue │ │ ├── search │ │ └── Search.vue │ │ ├── tabs │ │ ├── admin │ │ │ ├── AdminAuthority.vue │ │ │ ├── AdminMail.vue │ │ │ ├── AdminOverview.vue │ │ │ ├── AdminSensitive.vue │ │ │ ├── AdminTag.vue │ │ │ └── AdminUser.vue │ │ └── main │ │ │ ├── AllPosts.vue │ │ │ ├── HotPosts.vue │ │ │ └── LatestPosts.vue │ │ ├── tag │ │ ├── Tag.vue │ │ └── Tags.vue │ │ └── user │ │ ├── Profile.vue │ │ ├── RoleTest.vue │ │ └── Setting.vue └── vite.config.js ├── README.md └── flip.sql /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/flip.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 13 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Flip-back/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/FlipBackApplication.java: -------------------------------------------------------------------------------- 1 | package com.flip; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 7 | 8 | @SpringBootApplication 9 | @MapperScan("com.flip.mapper") 10 | @EnableAspectJAutoProxy 11 | public class FlipBackApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(FlipBackApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/annotation/LimitRequest.java: -------------------------------------------------------------------------------- 1 | package com.flip.annotation; 2 | 3 | import org.springframework.core.Ordered; 4 | import org.springframework.core.annotation.Order; 5 | 6 | import java.lang.annotation.*; 7 | 8 | /** 9 | * 限制接口调用频率的注解 10 | */ 11 | @Documented 12 | @Target(ElementType.METHOD) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Order(Ordered.HIGHEST_PRECEDENCE) 15 | public @interface LimitRequest { 16 | 17 | /** 18 | * 第一个等待时长,单位秒,默认60秒 19 | */ 20 | long firstWaitTime() default 60; 21 | 22 | /** 23 | * 第二个等待时长,单位秒,默认300秒 24 | */ 25 | long secondWaitTime() default 300; 26 | 27 | /** 28 | * 第三个等待时长,单位秒,默认600秒 29 | */ 30 | long thirdWaitTime() default 600; 31 | 32 | /** 33 | * 极端等待时长,单位秒,默认3600秒 34 | */ 35 | long ultraWaitTime() default 3600; 36 | 37 | /** 38 | * 在规定时间间隔内可请求多少次接口,默认3次 39 | */ 40 | int limitRequestTime() default 3; 41 | 42 | /** 43 | * 时间间隔,单位分钟,默认5分钟 44 | */ 45 | int timeInterval() default 5; 46 | 47 | /** 48 | * 被限制请求后返回的错误信息 49 | */ 50 | String requestLimitedMsg() default "请求过于频繁"; 51 | } 52 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/common/Response.java: -------------------------------------------------------------------------------- 1 | package com.flip.common; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.flip.domain.enums.ResponseCode; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | @Data 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | public class Response implements Serializable { 12 | 13 | private Integer code; 14 | private String message; 15 | private T data; 16 | 17 | public Response(Integer code, String message) { 18 | this.code = code; 19 | this.message = message; 20 | } 21 | 22 | public Response(Integer code, T data) { 23 | this.code = code; 24 | this.data = data; 25 | } 26 | 27 | public Response(Integer code, String message, T data) { 28 | this.code = code; 29 | this.message = message; 30 | this.data = data; 31 | } 32 | 33 | public static Response success(Integer code, String message, T data) { 34 | return new Response<>(code, message, data); 35 | } 36 | 37 | public static Response success(String message, T data) { 38 | return new Response<>(ResponseCode.SUCCESS.getCode(), message, data); 39 | } 40 | 41 | public static Response success(Integer code, String message) { 42 | return new Response<>(code, message); 43 | } 44 | 45 | public static Response success(String message) { 46 | return new Response<>(ResponseCode.SUCCESS.getCode(), message); 47 | } 48 | 49 | public static Response failed(Integer code, String message, T data) { 50 | return new Response<>(code, message, data); 51 | } 52 | 53 | public static Response failed(String message, T data) { 54 | return new Response<>(ResponseCode.BAD_REQUEST.getCode(), message, data); 55 | } 56 | 57 | public static Response failed(Integer code, String message) { 58 | return new Response<>(code, message); 59 | } 60 | 61 | public static Response failed(String message) { 62 | return new Response<>(ResponseCode.BAD_REQUEST.getCode(), message); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | package com.flip.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | 7 | /** 8 | * CORS跨域配置(Spring版),该类不使用,改为通过 SpringSecurity 配置跨域。 9 | * 10 | * 给项目添加了 Spring Security 依赖之后,Spring 官方提供的三种跨域解决方案可能会失效。 11 | * 通过 `@CrossOrigin` 注解或者重写 `addCorsMappings` 方法配置跨域,都会失效。 12 | * 而通过 `CorsFilter` 配置的跨域,有没有失效要看过滤器的优先级。 13 | * 如果优先级高于 Spring Security 过滤器,即先于 Spring Security 执行,则 `CorsFilter` 所配置的跨域处理依然有效; 14 | * 如果优先级低于 Spring Security 过滤器,则 `CorsFilter` 所配置的跨域处理也会失效。 15 | */ 16 | public class CorsConfig implements WebMvcConfigurer { 17 | 18 | @Value("${cors.allowed-address}") 19 | private String corsAllowedOrigin; 20 | 21 | /* 因为引入了 Spring Security,所以这里不再使用 Spring 提供的跨域配置,转而使用 Spring Security 的跨域配置。 */ 22 | @Override 23 | public void addCorsMappings(CorsRegistry registry) { 24 | registry.addMapping("/**") 25 | .allowedOriginPatterns(corsAllowedOrigin) 26 | .allowCredentials(false) 27 | .allowedMethods("*") 28 | .allowedHeaders("*"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/config/ElasticConfig.java: -------------------------------------------------------------------------------- 1 | package com.flip.config; 2 | 3 | import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; 4 | import co.elastic.clients.elasticsearch.ElasticsearchClient; 5 | import co.elastic.clients.json.jackson.JacksonJsonpMapper; 6 | import co.elastic.clients.transport.rest_client.RestClientTransport; 7 | import org.apache.http.HttpHost; 8 | import org.elasticsearch.client.RestClient; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | public class ElasticConfig { 15 | 16 | @Value("${elasticsearch.host}") 17 | private String host; 18 | 19 | @Value("${elasticsearch.port}") 20 | private Integer port; 21 | 22 | public RestClientTransport transport() { 23 | RestClient restClient = RestClient.builder(new HttpHost(host, port)).build(); 24 | return new RestClientTransport(restClient, new JacksonJsonpMapper()); 25 | } 26 | 27 | @Bean 28 | public ElasticsearchClient elasticsearchClient() { 29 | return new ElasticsearchClient(transport()); 30 | } 31 | 32 | @Bean 33 | public ElasticsearchAsyncClient elasticsearchAsyncClient() { 34 | return new ElasticsearchAsyncClient(transport()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/config/MybatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package com.flip.config; 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType; 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * MybatisPlus 配置查询结果分页 11 | */ 12 | @Configuration 13 | public class MybatisPlusConfig { 14 | @Bean 15 | public MybatisPlusInterceptor mybatisPlusInterceptor() { 16 | MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); 17 | mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); 18 | return mybatisPlusInterceptor; 19 | } 20 | } -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/config/ObjectMapperConfig.java: -------------------------------------------------------------------------------- 1 | package com.flip.config; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.SerializationFeature; 7 | import com.fasterxml.jackson.databind.module.SimpleModule; 8 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 9 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 10 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; 11 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.core.annotation.Order; 15 | 16 | import java.time.LocalDateTime; 17 | import java.time.format.DateTimeFormatter; 18 | 19 | @Order(0) 20 | @Configuration 21 | public class ObjectMapperConfig { 22 | 23 | @Bean 24 | public ObjectMapper objectMapper() { 25 | 26 | ObjectMapper mapper = new ObjectMapper(); 27 | 28 | // 对于空的对象转json的时候不抛出错误 29 | mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); 30 | 31 | // 禁用遇到未知属性抛出异常 32 | mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 33 | 34 | // 序列化BigDecimal时不使用科学计数法输出 35 | mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true); 36 | 37 | // JSON Long ==> String,将所有的 Long 类型转换成 String 类型返回 38 | SimpleModule simpleModule = new SimpleModule(); 39 | simpleModule.addSerializer(Long.class, ToStringSerializer.instance); 40 | mapper.registerModule(simpleModule); 41 | 42 | // 日期和时间格式化 43 | JavaTimeModule javaTimeModule = new JavaTimeModule(); 44 | javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); 45 | javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); 46 | mapper.registerModule(javaTimeModule); 47 | 48 | return mapper; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.flip.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 8 | import org.springframework.data.redis.serializer.StringRedisSerializer; 9 | 10 | /** 11 | * 在 RedisTemplate 中,默认是使用 Java 字符串序列化,字符串序列化不能够提供可读性。 12 | * 最佳的是替换 RedisTemplate 的 key 为默认的字符串序列化,value 采用 Jackson 序列化。 13 | */ 14 | @Configuration 15 | public class RedisConfig { 16 | 17 | @Bean 18 | public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { 19 | RedisTemplate redisTemplate = new RedisTemplate<>(); 20 | redisTemplate.setConnectionFactory(lettuceConnectionFactory); 21 | 22 | /* 使用 GenericJackson2JsonRedisSerializer 替换默认序列化 */ 23 | GenericJackson2JsonRedisSerializer jacksonSerializer = new GenericJackson2JsonRedisSerializer(); 24 | 25 | /* 设置 key 和 value 的序列化规则(key is String,value is JSON) */ 26 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 27 | redisTemplate.setValueSerializer(jacksonSerializer); 28 | redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 29 | redisTemplate.setHashValueSerializer(jacksonSerializer); 30 | 31 | /* 序列化完成 */ 32 | redisTemplate.afterPropertiesSet(); 33 | return redisTemplate; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.flip.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | public class WebConfig implements WebMvcConfigurer { 10 | 11 | @Value("${upload.avatarPath}") 12 | private String avatarPath; /*上传的头像存储的地址*/ 13 | 14 | @Value("${upload.avatarMapperPath}") 15 | private String avatarMapperPath; /*上传的头像映射的虚拟路径地址*/ 16 | 17 | @Value("${upload.staticPath}") 18 | private String staticPath; 19 | 20 | @Value("${upload.staticMapperPath}") 21 | private String staticMapperPath; 22 | 23 | @Override 24 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 25 | registry.addResourceHandler(staticMapperPath + "**").addResourceLocations("file:\\" + staticPath); 26 | registry.addResourceHandler(avatarMapperPath + "**").addResourceLocations("file:\\" + avatarPath); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.flip.controller; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import cn.hutool.http.HtmlUtil; 5 | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; 6 | import com.flip.common.Response; 7 | import com.flip.domain.entity.Comment; 8 | import com.flip.domain.entity.Post; 9 | import com.flip.service.CommentService; 10 | import com.flip.service.PostService; 11 | import com.flip.service.SensitiveWordService; 12 | import com.flip.utils.SensitiveWordUtils; 13 | import jakarta.annotation.Resource; 14 | import lombok.RequiredArgsConstructor; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | @RestController 21 | @RequiredArgsConstructor 22 | public class CommentController { 23 | 24 | private final CommentService commentService; 25 | 26 | private final SensitiveWordService sensitiveWordService; 27 | 28 | private final PostService postService; 29 | 30 | @GetMapping("/getComments") 31 | public Response> getComments(@RequestParam String pid) { 32 | List comments = commentService.getComments(pid); 33 | if (ObjectUtil.isNotNull(comments)) { 34 | for (Comment comment: comments) { 35 | comment.setContent(HtmlUtil.unescape(comment.getContent())); 36 | comment.setRepliesNum(commentService.getRepliesNumOfComment(comment.getId())); 37 | } 38 | return Response.success("获取评论列表成功", Map.of("comments", comments)); 39 | } 40 | return Response.failed("获取评论列表失败"); 41 | } 42 | 43 | @GetMapping("/getReplies") 44 | public Response getReplies(@RequestParam String pid, @RequestParam Integer parentId) { 45 | List replies = commentService.getReplies(pid, parentId); 46 | if (ObjectUtil.isNotNull(replies)) { 47 | for (Comment reply: replies) { 48 | reply.setContent(HtmlUtil.unescape(reply.getContent())); 49 | } 50 | return Response.success("获取回复列表成功", Map.of("replies", replies)); 51 | } 52 | return Response.failed("获取回复列表失败"); 53 | } 54 | 55 | @PostMapping("/doComment") 56 | public Response> doComment(@RequestBody Comment comment) { 57 | List sensitiveWords = sensitiveWordService.getSensitiveStringWords(); 58 | String commentContent = SensitiveWordUtils.stringSearchEx2Filter(comment.getContent(), sensitiveWords); 59 | comment.setContent(HtmlUtil.escape(commentContent)); 60 | comment.setParentId(0); 61 | comment.setReplyId(0); 62 | 63 | boolean saved = commentService.save(comment); 64 | if (saved) { 65 | UpdateWrapper updateWrapper = new UpdateWrapper<>(); 66 | updateWrapper.eq("id", comment.getPid()) 67 | .setSql("reply_number = reply_number + 1") 68 | .setSql("last_comment_time = '" + comment.getCreateTime() + "'"); 69 | 70 | boolean updated = postService.update(null, updateWrapper); 71 | if (updated) { 72 | comment.setContent(commentContent); 73 | return Response.success("评论成功", Map.of("comment", comment)); 74 | } 75 | } 76 | return Response.failed("评论失败"); 77 | } 78 | 79 | @PostMapping("/doReply") 80 | public Response> doReply(@RequestBody Comment comment) { 81 | List sensitiveWords = sensitiveWordService.getSensitiveStringWords(); 82 | String replyText = SensitiveWordUtils.stringSearchEx2Filter(comment.getContent(), sensitiveWords); 83 | comment.setContent(HtmlUtil.escape(replyText)); 84 | 85 | boolean saved = commentService.save(comment); 86 | if (saved) { 87 | comment.setContent(replyText); 88 | return Response.success("回复成功", Map.of("reply", comment)); 89 | } 90 | 91 | return Response.failed("回复失败"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package com.flip.controller; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import co.elastic.clients.elasticsearch.ElasticsearchClient; 5 | import co.elastic.clients.elasticsearch.core.search.Hit; 6 | import com.flip.common.Response; 7 | import com.flip.domain.entity.Post; 8 | import com.flip.domain.entity.User; 9 | import com.flip.domain.enums.ResponseCode; 10 | import com.flip.service.PostService; 11 | import com.flip.service.UserService; 12 | import com.flip.utils.elastic.ElasticPostUtils; 13 | import com.flip.utils.elastic.ElasticUserUtils; 14 | import lombok.RequiredArgsConstructor; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import java.io.IOException; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | @RestController 23 | @RequestMapping("/search") 24 | @RequiredArgsConstructor 25 | public class SearchController { 26 | 27 | private final ElasticsearchClient elasticsearchClient; 28 | 29 | private final PostService postService; 30 | 31 | private final UserService userService; 32 | 33 | @GetMapping("/post") 34 | public Response>> searchPostsByKey(@RequestParam String keyword) { 35 | try { 36 | List> postHits = ElasticPostUtils.searchPostsByKey(elasticsearchClient, keyword); 37 | if (ObjectUtil.isNull(postHits)) { 38 | return Response.success("没有与关键词匹配的结果"); 39 | } else { 40 | List searchedPosts = new ArrayList<>(); 41 | postHits.forEach(postHit -> { 42 | searchedPosts.add(postHit.source()); 43 | }); 44 | return Response.success("查询成功", Map.of("posts", searchedPosts)); 45 | } 46 | } catch (IOException e) { 47 | return Response.failed(ResponseCode.INTERNAL_SERVER_ERROR.getCode(), "服务器错误,请稍后重试"); 48 | } 49 | } 50 | 51 | @GetMapping("/user") 52 | public Response>> searchUsersByKey(@RequestParam String keyword) { 53 | try { 54 | List> userHits = ElasticUserUtils.searchUsersByKey(elasticsearchClient, keyword); 55 | if (ObjectUtil.isNull(userHits)) { 56 | return Response.success("没有与关键词匹配的结果"); 57 | } else { 58 | List searchedUsers = new ArrayList<>(); 59 | userHits.forEach(userHit -> { 60 | searchedUsers.add(userHit.source()); 61 | }); 62 | return Response.success("查询成功", Map.of("users", searchedUsers)); 63 | } 64 | } catch (IOException e) { 65 | return Response.failed(ResponseCode.INTERNAL_SERVER_ERROR.getCode(), "服务器错误,请稍后重试"); 66 | } 67 | } 68 | 69 | @PostMapping("/addPosts") 70 | public Response insertAllPostToEs() { 71 | boolean success; 72 | try { 73 | success = ElasticPostUtils.insertAllPostByBulkOperation(elasticsearchClient, postService); 74 | } catch (IOException e) { 75 | return Response.failed("插入数据失败"); 76 | } 77 | if (success) { 78 | return Response.success("插入数据成功"); 79 | } else return Response.failed("插入数据失败"); 80 | } 81 | 82 | @PostMapping("/addUsers") 83 | public Response insertAllUserToEs() { 84 | boolean success; 85 | try { 86 | success = ElasticUserUtils.insertAllUserByBulkOperation(elasticsearchClient, userService); 87 | } catch (IOException e) { 88 | return Response.failed("插入数据失败"); 89 | } 90 | if (success) { 91 | return Response.success("插入数据成功"); 92 | } else return Response.failed("插入数据失败"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/controller/SensitiveController.java: -------------------------------------------------------------------------------- 1 | package com.flip.controller; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 4 | import com.flip.common.Response; 5 | import com.flip.domain.entity.SensitiveWord; 6 | import com.flip.service.SensitiveWordService; 7 | import com.flip.utils.RedisKeyUtils; 8 | import jakarta.annotation.Resource; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | @Slf4j 18 | @RestController 19 | @RequestMapping("/sys-ctrl") 20 | @RequiredArgsConstructor 21 | public class SensitiveController { 22 | 23 | private final String sensitiveObjsKey = RedisKeyUtils.getSensitiveObjsKey(); 24 | 25 | private final SensitiveWordService sensitiveWordService; 26 | 27 | private final RedisTemplate redisTemplate; 28 | 29 | @PostMapping("/sensitiveWord") 30 | public Response> addSensitiveWord(@RequestBody SensitiveWord sensitiveWord) { 31 | boolean saved = sensitiveWordService.save(sensitiveWord); 32 | if (saved) { 33 | redisTemplate.opsForList().rightPushIfPresent(sensitiveObjsKey, sensitiveWord); 34 | return Response.success("新增敏感词成功", Map.of("sensitiveWord", sensitiveWord)); 35 | } else return Response.failed("新增敏感词失败"); 36 | } 37 | 38 | @GetMapping("/sensitiveWord") 39 | public Response>> getSensitiveWords() { 40 | List sensitiveWords = sensitiveWordService.getSensitiveWords(); 41 | return Response.success("获取敏感词成功", Map.of("sensitiveWords", sensitiveWords)); 42 | } 43 | 44 | @PutMapping("/sensitiveWord") 45 | public Response> updateSensitiveWord(@RequestBody SensitiveWord sensitiveWord) { 46 | QueryWrapper queryWrapper = new QueryWrapper<>(); 47 | queryWrapper.eq("id", sensitiveWord.getId()); 48 | if (!sensitiveWordService.exists(queryWrapper)) { 49 | return Response.failed("该敏感词不存在"); 50 | } 51 | 52 | boolean updated = sensitiveWordService.updateById(sensitiveWord); 53 | if (updated) { 54 | redisTemplate.opsForList().set(sensitiveObjsKey, sensitiveWord.getId() - 1, sensitiveWord); 55 | return Response.success("更新敏感词成功", Map.of("sensitiveWord", sensitiveWord)); 56 | } else return Response.failed("更新敏感词失败"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/controller/StatusController.java: -------------------------------------------------------------------------------- 1 | package com.flip.controller; 2 | 3 | import com.flip.common.Response; 4 | import com.flip.mapper.PostMapper; 5 | import com.flip.mapper.UserMapper; 6 | import com.flip.utils.RedisKeyUtils; 7 | import com.flip.utils.SystemUtils; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @RestController 17 | @RequiredArgsConstructor 18 | public class StatusController { 19 | 20 | private final RedisTemplate redisTemplate; 21 | 22 | private final UserMapper userMapper; 23 | 24 | private final PostMapper postMapper; 25 | 26 | @GetMapping("/systemInfo") 27 | public Response getSystemInfo() { 28 | Map memory = SystemUtils.getMemory(); 29 | Map processor = SystemUtils.getProcessor(); 30 | Map forum = getForumInfo().getData(); 31 | 32 | Map systemInfo = new HashMap<>(); 33 | systemInfo.put("memory", memory); 34 | systemInfo.put("cpu", processor); 35 | systemInfo.put("forum", forum); 36 | return Response.success("获取系统信息成功", systemInfo); 37 | } 38 | 39 | @GetMapping("/forumInfo") 40 | public Response> getForumInfo() { 41 | String userNumberKey = RedisKeyUtils.getUserNumberKey(); 42 | String postNumberKey = RedisKeyUtils.getPostNumberKey(); 43 | if (Boolean.FALSE.equals(redisTemplate.hasKey(userNumberKey))) { 44 | Long userNumber = userMapper.selectCount(null); 45 | redisTemplate.opsForValue().set(userNumberKey, userNumber); 46 | } 47 | if (Boolean.FALSE.equals(redisTemplate.hasKey(postNumberKey))) { 48 | Long postNumber = postMapper.selectCount(null); 49 | redisTemplate.opsForValue().set(postNumberKey, postNumber); 50 | } 51 | 52 | Map map = new HashMap<>(); 53 | map.put("userNumber", (Integer) redisTemplate.opsForValue().get(userNumberKey)); 54 | map.put("postNumber", (Integer) redisTemplate.opsForValue().get(postNumberKey)); 55 | return Response.success("获取社区数据成功", map); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.flip.controller; 2 | 3 | import com.flip.common.Response; 4 | import org.springframework.security.access.prepost.PreAuthorize; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @RestController 9 | public class TestController { 10 | 11 | @PreAuthorize("hasRole('MODERATOR')") 12 | @GetMapping("/testRole") 13 | public Response testRole() { 14 | return Response.success("测试角色成功"); 15 | } 16 | 17 | @GetMapping("/testPermission") 18 | public Response testPermission() { 19 | return Response.success("测试权限成功"); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/controller/UserSysController.java: -------------------------------------------------------------------------------- 1 | package com.flip.controller; 2 | 3 | import co.elastic.clients.elasticsearch.ElasticsearchClient; 4 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 5 | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; 6 | import com.flip.common.Response; 7 | import com.flip.domain.entity.Authority; 8 | import com.flip.domain.entity.BannedUser; 9 | import com.flip.domain.entity.Role; 10 | import com.flip.domain.entity.User; 11 | import com.flip.service.BannedUserService; 12 | import com.flip.service.UserService; 13 | import com.flip.utils.elastic.ElasticUserUtils; 14 | import lombok.RequiredArgsConstructor; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import java.io.IOException; 18 | import java.time.LocalDateTime; 19 | import java.time.format.DateTimeFormatter; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | @RestController 24 | @RequestMapping("/sys-ctrl") 25 | @RequiredArgsConstructor 26 | public class UserSysController { 27 | 28 | private final UserService userService; 29 | 30 | private final BannedUserService bannedUserService; 31 | 32 | private final ElasticsearchClient elasticsearchClient; 33 | 34 | @GetMapping("/users") 35 | public Response>> getAllUser() { 36 | List users = userService.list(); 37 | users.forEach(user -> { 38 | Role role = userService.loadUserRoleByUid(String.valueOf(user.getUid())); 39 | Authority authority = new Authority(); 40 | authority.setAuthorities(userService.loadRoleAuthoritiesByRid(role.getRid())); 41 | 42 | user.setPassword(null); 43 | user.setRole(role); 44 | user.setAuthority(authority); 45 | }); 46 | 47 | return Response.success("获取所有用户成功", Map.of("users", users)); 48 | } 49 | 50 | @PostMapping("/banUser") 51 | public Response banUser(@RequestBody BannedUser bannedUser) { 52 | boolean saved = bannedUserService.save(bannedUser); 53 | if (saved) { 54 | Long uid = bannedUser.getUid(); 55 | String reason = bannedUser.getReason(); 56 | String deadline = bannedUser.getDeadline(); 57 | String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); 58 | bannedUserService.insertBannedHistory(uid, now, deadline, reason); 59 | 60 | UpdateWrapper userUpdateWrapper = new UpdateWrapper<>(); 61 | userUpdateWrapper.eq("uid", uid).set("banned", true); 62 | userService.update(null, userUpdateWrapper); 63 | 64 | User user = new User(); 65 | user.setUid(uid); 66 | user.setBanned(true); 67 | try { 68 | ElasticUserUtils.updateUserBannedStatusInEs(elasticsearchClient, user); 69 | } catch (IOException e) { 70 | return Response.success("封禁成功"); 71 | } 72 | return Response.success("封禁成功"); 73 | } else return Response.failed("封禁失败"); 74 | } 75 | 76 | @DeleteMapping("/banUser") 77 | public Response cancelBanUser(@RequestParam String uid) { 78 | QueryWrapper queryWrapper = new QueryWrapper<>(); 79 | queryWrapper.eq("uid", uid); 80 | 81 | boolean removed = bannedUserService.remove(queryWrapper); 82 | if (removed) { 83 | UpdateWrapper userUpdateWrapper = new UpdateWrapper<>(); 84 | userUpdateWrapper.eq("uid", uid).set("banned", false); 85 | userService.update(null, userUpdateWrapper); 86 | 87 | User user = new User(); 88 | user.setUid(Long.valueOf(uid)); 89 | user.setBanned(false); 90 | try { 91 | ElasticUserUtils.updateUserBannedStatusInEs(elasticsearchClient, user); 92 | } catch (IOException e) { 93 | return Response.success("解除封禁成功"); 94 | } 95 | return Response.success("解除封禁成功"); 96 | } else return Response.failed("解除封禁失败"); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/dto/LoggedUser.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.dto; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.flip.domain.entity.User; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import lombok.ToString; 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | 14 | import java.util.Collection; 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | 18 | /** 19 | * 已认证(登录)的用户相关信息 20 | */ 21 | @Data 22 | @ToString 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public class LoggedUser implements UserDetails { 26 | 27 | private User user; 28 | 29 | @Override 30 | @JsonIgnore 31 | public Collection getAuthorities() { 32 | Set authorities = new HashSet<>(); 33 | if (ObjectUtil.isNotNull(user.getRole())) { 34 | authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole().getName())); 35 | } 36 | if (ObjectUtil.isNotNull(user.getAuthority())) { 37 | user.getAuthority().getAuthorities().forEach(authority -> { 38 | authorities.add(new SimpleGrantedAuthority(authority)); 39 | }); 40 | } 41 | return authorities; 42 | } 43 | 44 | @Override 45 | @JsonIgnore 46 | public String getPassword() { 47 | return user.getPassword(); 48 | } 49 | 50 | @Override 51 | @JsonIgnore 52 | public String getUsername() { 53 | return user.getUsername(); 54 | } 55 | 56 | @Override 57 | @JsonIgnore 58 | public boolean isAccountNonExpired() { 59 | return true; 60 | } 61 | 62 | @Override 63 | @JsonIgnore 64 | public boolean isAccountNonLocked() { 65 | return true; 66 | } 67 | 68 | @Override 69 | @JsonIgnore 70 | public boolean isCredentialsNonExpired() { 71 | return true; 72 | } 73 | 74 | @Override 75 | @JsonIgnore 76 | public boolean isEnabled() { 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/Authority.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | import java.util.List; 11 | 12 | @Data 13 | @ToString 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class Authority { 17 | 18 | @TableId(type = IdType.AUTO) 19 | private Integer id; 20 | 21 | private String name; 22 | 23 | private List authorities; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/BannedHistory.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | @Data 11 | @ToString 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class BannedHistory { 15 | 16 | @TableId(type = IdType.AUTO) 17 | private Integer id; 18 | 19 | /** 20 | * 封禁用户的UID 21 | */ 22 | private Long uid; 23 | 24 | /** 25 | * 封禁起始时间 26 | */ 27 | private String createTime; 28 | 29 | /** 30 | * 解封截止时间 31 | */ 32 | private String deadline; 33 | 34 | /** 35 | * 封禁理由 36 | */ 37 | private String reason; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/BannedUser.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | 9 | @Data 10 | @ToString 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @TableName("banned") 14 | public class BannedUser { 15 | 16 | @TableId(type = IdType.AUTO) 17 | private Integer id; 18 | 19 | /** 20 | * 被封禁的用户UID 21 | */ 22 | private Long uid; 23 | 24 | /** 25 | * 解封截止时间 26 | */ 27 | private String deadline; 28 | 29 | /** 30 | * 封禁理由 31 | */ 32 | private String reason; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.IdType; 5 | import com.baomidou.mybatisplus.annotation.TableField; 6 | import com.baomidou.mybatisplus.annotation.TableId; 7 | import jakarta.validation.constraints.NotBlank; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @Data 14 | @ToString 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class Comment { 18 | 19 | @TableId(type = IdType.AUTO) 20 | private Integer id; 21 | 22 | /** 23 | * 评论或回复对应的帖子ID 24 | */ 25 | private Long pid; 26 | 27 | /** 28 | * 回复者UID 29 | */ 30 | private Long fromUid; 31 | 32 | /** 33 | * 被回复者UID 34 | */ 35 | private Long toUid; 36 | 37 | /** 38 | * 评论或回复的内容 39 | */ 40 | @NotBlank(message = "评论内容不能为空") 41 | private String content; 42 | 43 | /** 44 | * 子级回复的父级回复ID,根级评论值为0 45 | */ 46 | private Integer parentId; 47 | 48 | /** 49 | * 楼中楼中回复目标楼层的ID 50 | */ 51 | private Integer replyId; 52 | 53 | /** 54 | * 评论or回复时间,在插入记录时自动填充 55 | */ 56 | @TableField(fill = FieldFill.INSERT) 57 | private String createTime; 58 | 59 | /** 60 | * 论或回复者的用户名 61 | */ 62 | @TableField(exist = false) 63 | private String fromUsername; 64 | 65 | /** 66 | * 评论或回复者的昵称 67 | */ 68 | @TableField(exist = false) 69 | private String fromNickname; 70 | 71 | /** 72 | * 评论或回复者的头像 73 | */ 74 | @TableField(exist = false) 75 | private String fromAvatar; 76 | 77 | /** 78 | * 被评论或被回复者的用户名 79 | */ 80 | @TableField(exist = false) 81 | private String toUsername; 82 | 83 | /** 84 | * 被评论或被回复者的昵称 85 | */ 86 | @TableField(exist = false) 87 | private String toNickname; 88 | 89 | /** 90 | * 评论对应的回复数量 91 | */ 92 | @TableField(exist = false) 93 | private Integer repliesNum; 94 | 95 | } 96 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/Post.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import jakarta.validation.constraints.NotBlank; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | 12 | import java.util.List; 13 | 14 | @Data 15 | @ToString 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class Post { 19 | 20 | @TableId 21 | private Long id; 22 | 23 | private Long uid; 24 | 25 | @NotBlank(message = "标题不能为空") 26 | private String title; 27 | 28 | @NotBlank(message = "内容不能为空") 29 | private String content; 30 | 31 | /** 32 | * 帖子优先级 33 | */ 34 | private Double priority; 35 | 36 | /** 37 | * 帖子类型。0-正常; 1-置顶; 2-全局置顶 38 | */ 39 | private Integer type; 40 | 41 | /** 42 | * 帖子状态类型。0-正常; 1-精华; 2-拉黑 43 | */ 44 | private Integer status; 45 | 46 | /** 47 | * 帖子发布时间,在插入记录时自动填充 48 | */ 49 | @TableField(fill = FieldFill.INSERT) 50 | private String createTime; 51 | 52 | /** 53 | * 帖子被回复数 54 | */ 55 | private Integer replyNumber; 56 | 57 | /** 58 | * 帖子被查看数 59 | */ 60 | private Integer viewNumber; 61 | 62 | /** 63 | * 当前帖子最新一条评论的时间,在插入记录时自动填充 64 | */ 65 | @TableField(fill = FieldFill.INSERT) 66 | private String lastCommentTime; 67 | 68 | /** 69 | * 帖子标签 70 | */ 71 | @TableField(exist = false) 72 | private List tags; 73 | 74 | /** 75 | * 作者用户名 76 | */ 77 | @TableField(exist = false) 78 | private String author; 79 | 80 | /** 81 | * 作者昵称 82 | */ 83 | @TableField(exist = false) 84 | private String nickname; 85 | 86 | /** 87 | * 作者头像 88 | */ 89 | @TableField(exist = false) 90 | private String avatar; 91 | 92 | } 93 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/PostTag.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.IdType; 5 | import com.baomidou.mybatisplus.annotation.TableField; 6 | import com.baomidou.mybatisplus.annotation.TableId; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | 12 | @Data 13 | @ToString 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class PostTag { 17 | 18 | @TableId(type = IdType.AUTO) 19 | private Integer id; 20 | 21 | /** 22 | * 帖子ID 23 | */ 24 | private Long pid; 25 | 26 | /** 27 | * 帖子标签 28 | */ 29 | private String tagLabel; 30 | 31 | /** 32 | * 标签创建者 33 | */ 34 | private Long creator; 35 | 36 | /** 37 | * 标签创建时间 38 | */ 39 | @TableField(fill = FieldFill.INSERT) 40 | private String createTime; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/Role.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | @Data 11 | @ToString 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class Role { 15 | 16 | @TableId(type = IdType.AUTO) 17 | private Integer rid; 18 | 19 | /** 20 | * 角色名 21 | */ 22 | private String name; 23 | 24 | /** 25 | * 角色别名 26 | */ 27 | private String alias; 28 | 29 | /** 30 | * 角色排序索引,越小越靠前 31 | */ 32 | private Integer index; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/SensitiveWord.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | @Data 11 | @ToString 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class SensitiveWord { 15 | 16 | @TableId(type = IdType.AUTO) 17 | private Integer Id; 18 | 19 | /** 20 | * 敏感词 21 | */ 22 | private String word; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/Tag.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.IdType; 5 | import com.baomidou.mybatisplus.annotation.TableField; 6 | import com.baomidou.mybatisplus.annotation.TableId; 7 | import com.flip.validation.VG; 8 | import jakarta.validation.constraints.NotBlank; 9 | import jakarta.validation.constraints.NotNull; 10 | import jakarta.validation.constraints.Pattern; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Data; 13 | import lombok.NoArgsConstructor; 14 | import lombok.ToString; 15 | 16 | @Data 17 | @ToString 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class Tag { 21 | 22 | @TableId(type = IdType.AUTO) 23 | private Integer id; 24 | 25 | /** 26 | * 标签类型 27 | */ 28 | @NotNull(message = "标签类型不能为空", groups = VG.Fifth.class) 29 | private Integer optionId; 30 | 31 | /** 32 | * 标签名 33 | */ 34 | @NotBlank(message = "标签名不能为空", groups = VG.First.class) 35 | @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9]+$", message = "标签名不能包含特殊字符", groups = VG.Second.class) 36 | private String name; 37 | 38 | /** 39 | * 标签英文标识 40 | */ 41 | @NotBlank(message = "标签英文标识不能为空", groups = VG.Third.class) 42 | @Pattern(regexp = "^[A-Za-z\\d\\-]+$", message = "标签英文标识不能包含特殊字符", groups = VG.Fourth.class) 43 | private String label; 44 | 45 | /** 46 | * 标签图标 47 | */ 48 | private String icon; 49 | 50 | /** 51 | * 标签详情 52 | */ 53 | private String detail; 54 | 55 | /** 56 | * 标签创建者 57 | */ 58 | private Long creator; 59 | 60 | /** 61 | * 标签创建时间,在插入记录时自动填充 62 | */ 63 | @TableField(fill = FieldFill.INSERT) 64 | private String createTime; 65 | 66 | /** 67 | * 标签选项 68 | */ 69 | @TableField(exist = false) 70 | private TagOption tagOption; 71 | 72 | public Tag(Integer optionId, String name, String label, String icon, String detail) { 73 | this.optionId = optionId; 74 | this.name = name; 75 | this.label = label; 76 | this.icon = icon; 77 | this.detail = detail; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/TagOption.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.flip.validation.VG; 6 | import jakarta.validation.constraints.NotBlank; 7 | import jakarta.validation.constraints.Pattern; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @Data 14 | @ToString 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class TagOption { 18 | 19 | @TableId(type = IdType.AUTO) 20 | private Integer id; 21 | 22 | /** 23 | * 标签类型的显示名称 24 | */ 25 | @NotBlank(message = "类型的显示名称不能为空", groups = VG.First.class) 26 | @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9]+$", message = "类型的显示名称不能包含特殊字符", groups = VG.Second.class) 27 | private String name; 28 | 29 | /** 30 | * 标签类型的英文标识 31 | */ 32 | @NotBlank(message = "类型的英文标识不能为空", groups = VG.Third.class) 33 | @Pattern(regexp = "^[A-Za-z\\d\\-]+$", message = "类型英文标识不能包含特殊字符", groups = VG.Fourth.class) 34 | private String label; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | import com.flip.validation.VG; 5 | import jakarta.validation.constraints.Email; 6 | import jakarta.validation.constraints.NotBlank; 7 | import jakarta.validation.constraints.Pattern; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | import org.hibernate.validator.constraints.Length; 13 | 14 | @Data 15 | @ToString 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class User { 19 | 20 | private Integer id; 21 | 22 | /** 23 | * 用户UID 24 | */ 25 | @TableId(type = IdType.ASSIGN_ID) 26 | private Long uid; 27 | 28 | /** 29 | * 用户头像 30 | */ 31 | private String avatar; 32 | 33 | /** 34 | * 用户名 35 | */ 36 | @NotBlank(message = "用户名不能为空", groups = VG.First.class) 37 | @Length(min = 4, max = 12, message = "最少4个字符,最多12个字符", groups = VG.Second.class) 38 | @Pattern(regexp = "^[A-Za-z\\d]+$", message = "仅限英文字符或数字", groups = VG.Third.class) 39 | private String username; 40 | 41 | /** 42 | * 用户昵称 43 | */ 44 | private String nickname; 45 | 46 | /** 47 | * 用户邮箱 48 | */ 49 | @NotBlank(message = "邮箱不能为空", groups = VG.Fourth.class) 50 | @Email(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$", message = "不支持的邮箱格式", groups = VG.Fifth.class) 51 | private String email; 52 | 53 | /** 54 | * 用户密码 55 | */ 56 | @NotBlank(message = "密码不能为空", groups = VG.Sixth.class) 57 | @Length(min = 8, message = "密码长度最少八位", groups = VG.Seventh.class) 58 | @Pattern(regexp = "^(?=.*\\d.*)(?=.*[A-Z].*)(?=.*[a-z].*)(?=.*[`~!@#$%^&*()_\\-+=<>.?:\"{}].*).{8,20}$", message = "需同时包含大小写字母、数字和特殊符号", groups = VG.Eighth.class) 59 | private String password; 60 | 61 | /** 62 | * 邮箱认证状态 63 | */ 64 | private Boolean emailVerified; 65 | 66 | /** 67 | * 用户盐值 68 | */ 69 | private String salt; 70 | 71 | /** 72 | * 注册IP 73 | */ 74 | private String registerIp; 75 | 76 | /** 77 | * 资料被谁更新 78 | */ 79 | private String updatedBy; 80 | 81 | /** 82 | * 注册时间 83 | */ 84 | @TableField(fill = FieldFill.INSERT) 85 | private String createTime; 86 | 87 | /** 88 | * 更新账号信息时间 89 | */ 90 | @TableField(fill = FieldFill.UPDATE) 91 | private String updateTime; 92 | 93 | /** 94 | * 是否被禁用。{ false:未禁用, true:已禁用 } 95 | */ 96 | private Boolean banned; 97 | 98 | /** 99 | * 是否被(逻辑)删除。{ false:未删除, true:已删除 } 100 | */ 101 | @TableLogic 102 | private Boolean deleted; 103 | 104 | /** 105 | * 用户具有的权限 106 | */ 107 | @TableField(exist = false) 108 | private Authority authority; 109 | 110 | /** 111 | * 用户的角色信息 112 | */ 113 | @TableField(exist = false) 114 | private Role role; 115 | 116 | } 117 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/domain/enums/ResponseCode.java: -------------------------------------------------------------------------------- 1 | package com.flip.domain.enums; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum ResponseCode { 7 | 8 | SUCCESS(200, "请求执行成功"), 9 | 10 | /** 11 | * 遇到错误,用户需求无法完成 12 | */ 13 | BAD_REQUEST(400, "请求执行出现错误"), 14 | 15 | /** 16 | * accessToken 过期 17 | */ 18 | UNAUTHORIZED(401, "未授权"), 19 | 20 | /** 21 | * 用户权限不足 22 | */ 23 | FORBIDDEN(403, "禁止访问"), 24 | 25 | /** 26 | * 资源不存在 27 | */ 28 | NOT_FOUND(404, "资源不存在"), 29 | 30 | /** 31 | * 请求携带错误类型的 token,如需要 accessToken 但解析得到的是 refreshToken 32 | */ 33 | PRECONDITION_FAILED(412, "前提条件不匹配"), 34 | 35 | /** 36 | * accessToken 与 refreshToken 双双过期 37 | */ 38 | AUTHENTICATION_EXPIRED(419, "身份过期"), 39 | 40 | /** 41 | * 内部错误 42 | */ 43 | INTERNAL_SERVER_ERROR(500, "服务器内部错误"); 44 | 45 | private final Integer code; 46 | private final String message; 47 | 48 | ResponseCode(Integer code, String message) { 49 | this.code = code; 50 | this.message = message; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/exception/InvalidTokenException.java: -------------------------------------------------------------------------------- 1 | package com.flip.exception; 2 | 3 | import io.jsonwebtoken.JwtException; 4 | 5 | /** 6 | * 无效Token异常 7 | */ 8 | public class InvalidTokenException extends JwtException { 9 | 10 | public InvalidTokenException(String message) { 11 | super(message); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/handler/AutoFillHandler.java: -------------------------------------------------------------------------------- 1 | package com.flip.handler; 2 | 3 | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 4 | import org.apache.ibatis.reflection.MetaObject; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.time.LocalDateTime; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | /** 11 | * 字段自动填充处理类。 12 | */ 13 | @Component 14 | public class AutoFillHandler implements MetaObjectHandler { 15 | 16 | DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 17 | 18 | @Override 19 | public void insertFill(MetaObject metaObject) { 20 | strictInsertFill(metaObject, "createTime", String.class, LocalDateTime.now().format(dateTimeFormatter)); 21 | strictInsertFill(metaObject, "lastCommentTime", String.class, LocalDateTime.now().format(dateTimeFormatter)); 22 | } 23 | 24 | @Override 25 | public void updateFill(MetaObject metaObject) { 26 | strictUpdateFill(metaObject, "updateTime", String.class, LocalDateTime.now().format(dateTimeFormatter)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/handler/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.flip.handler; 2 | 3 | import com.flip.common.Response; 4 | import com.flip.domain.enums.ResponseCode; 5 | import jakarta.validation.ConstraintViolation; 6 | import jakarta.validation.ConstraintViolationException; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.MissingServletRequestParameterException; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | import org.springframework.web.bind.annotation.RestControllerAdvice; 14 | 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.stream.Collectors; 18 | 19 | @RestControllerAdvice 20 | public class GlobalExceptionHandler { 21 | 22 | /** 23 | * 全局处理数据校验不一致产生的异常,当 @Validated 修饰 java 对象时引起 24 | * @param e 错误类型对象 25 | * @return 响应体 26 | */ 27 | @ExceptionHandler({MethodArgumentNotValidException.class}) 28 | public Response> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { 29 | Map map = new HashMap<>(); 30 | e.getBindingResult().getAllErrors().forEach((error) -> { 31 | String fieldName = ((FieldError)error).getField(); 32 | String errorMsg = error.getDefaultMessage(); 33 | map.put(fieldName, errorMsg); 34 | }); 35 | return Response.failed("参数校验失败", map); 36 | } 37 | 38 | /** 39 | * 当 @Validated 修饰单个参数时(此时 @Validated 在类上而不是在方法中) 40 | * @param e 错误类型对象 41 | * @return 响应体 42 | */ 43 | @ExceptionHandler({ConstraintViolationException.class}) 44 | public Response handleConstraintViolationException(ConstraintViolationException e) { 45 | String message = e.getConstraintViolations().stream() 46 | .map(ConstraintViolation::getMessage) 47 | .collect(Collectors.joining(",")); 48 | return Response.failed("参数校验失败", message); 49 | } 50 | 51 | /** 52 | * 全局处理参数未提供异常,异常由 @RequestParam(required = true) 引起。 53 | * @param e 错误类型对象 54 | * @return 响应体 55 | */ 56 | @ExceptionHandler({MissingServletRequestParameterException.class}) 57 | public Response handleMissingRequestParameterException(MissingServletRequestParameterException e) { 58 | String parameterName = e.getParameterName(); 59 | String message = "需要提供 " + parameterName + " 参数"; 60 | return Response.failed("参数缺失", message); 61 | } 62 | 63 | /** 64 | * authenticationManager.authenticate() 验证登录用户的用户名和密码时,如果匹配不成功,则会抛出AuthenticationException 65 | * @return 响应体 66 | */ 67 | @ExceptionHandler({AuthenticationException.class}) 68 | public Response handleAuthenticationException(AuthenticationException e) { 69 | return Response.failed(ResponseCode.FORBIDDEN.getCode(), "用户名或密码错误"); 70 | } 71 | 72 | @ExceptionHandler({UsernameNotFoundException.class}) 73 | public Response handleUsernameNotFoundException(UsernameNotFoundException e) { 74 | return Response.failed(ResponseCode.FORBIDDEN.getCode(), e.getMessage()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/handler/LogoutHandler.java: -------------------------------------------------------------------------------- 1 | package com.flip.handler; 2 | 3 | import com.flip.domain.dto.LoggedUser; 4 | import com.flip.utils.LoggedUserUtils; 5 | import com.flip.utils.RedisKeyUtils; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.data.redis.core.RedisTemplate; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; 14 | import org.springframework.stereotype.Component; 15 | 16 | @Slf4j 17 | @Component 18 | @RequiredArgsConstructor 19 | public class LogoutHandler extends SecurityContextLogoutHandler { 20 | 21 | private final RedisTemplate redisTemplate; 22 | 23 | @Override 24 | public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { 25 | LoggedUser loggedUser = LoggedUserUtils.getLoggedUser(); 26 | String loggedUserKey = RedisKeyUtils.getLoggedUserKey(String.valueOf(loggedUser.getUser().getUid())); 27 | 28 | redisTemplate.delete(loggedUserKey); 29 | SecurityContextHolder.clearContext(); 30 | log.info("用户 '{}' 已退出登录", loggedUser.getUser().getUsername()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/handler/LogoutSuccessHandlerImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.handler; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.flip.domain.enums.ResponseCode; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.IOException; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | @Slf4j 18 | @Component 19 | public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { 20 | 21 | @Override 22 | public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 23 | Map map = new HashMap<>(); 24 | map.put("code", ResponseCode.SUCCESS.getCode()); 25 | map.put("message", "已退出登录"); 26 | 27 | response.setContentType("application/json;charset=UTF-8"); 28 | 29 | String result = new ObjectMapper() 30 | .writerWithDefaultPrettyPrinter() 31 | .writeValueAsString(map); 32 | 33 | try { 34 | response.getWriter().println(result); 35 | } finally { 36 | response.getWriter().close(); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/handler/NoLoginHandler.java: -------------------------------------------------------------------------------- 1 | package com.flip.handler; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.flip.domain.enums.ResponseCode; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.security.web.AuthenticationEntryPoint; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.IOException; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * 匿名用户(未登录)访问权限资源时的处理 19 | */ 20 | @Component 21 | @RequiredArgsConstructor 22 | public class NoLoginHandler implements AuthenticationEntryPoint { 23 | private final ObjectMapper objectMapper; 24 | 25 | @Override 26 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { 27 | Map map = new HashMap<>(); 28 | map.put("code", ResponseCode.FORBIDDEN.getCode()); 29 | map.put("message", "请登录后再尝试"); 30 | 31 | response.setContentType("application/json;charset=UTF-8"); 32 | String result = objectMapper.writerWithDefaultPrettyPrinter() 33 | .writeValueAsString(map); 34 | 35 | try { 36 | response.getWriter().println(result); 37 | } finally { 38 | response.getWriter().close(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/handler/NoPermissionHandler.java: -------------------------------------------------------------------------------- 1 | package com.flip.handler; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.flip.domain.enums.ResponseCode; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.security.access.AccessDeniedException; 10 | import org.springframework.security.web.access.AccessDeniedHandler; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.IOException; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * 权限不足时的处理 19 | */ 20 | @Component 21 | @RequiredArgsConstructor 22 | public class NoPermissionHandler implements AccessDeniedHandler { 23 | 24 | private final ObjectMapper objectMapper; 25 | 26 | @Override 27 | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { 28 | Map map = new HashMap<>(); 29 | map.put("code", ResponseCode.FORBIDDEN.getCode()); 30 | map.put("message", "权限不足"); 31 | 32 | response.setContentType("application/json;charset=UTF-8"); 33 | String result = objectMapper.writerWithDefaultPrettyPrinter() 34 | .writeValueAsString(map); 35 | 36 | try { 37 | response.getWriter().println(result); 38 | } finally { 39 | response.getWriter().close(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/AuthorityMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.Authority; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | import java.util.List; 9 | 10 | @Mapper 11 | public interface AuthorityMapper extends BaseMapper { 12 | 13 | List loadRoleAuthoritiesByRid(@Param("rid") int rid); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/BannedUserMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.BannedUser; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface BannedUserMapper extends BaseMapper { 9 | 10 | void insertBannedHistory(Long uid, String createTime, String deadline, String reason); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.Comment; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface CommentMapper extends BaseMapper { 11 | 12 | List getComments(String pid); 13 | 14 | Integer getRepliesNumOfComment(Integer parentId); 15 | 16 | List getReplies(String pid, Integer parentId); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/PostMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.Post; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | import java.util.List; 8 | 9 | @Mapper 10 | public interface PostMapper extends BaseMapper { 11 | 12 | List getNewestPost(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/PostTagMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.PostTag; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface PostTagMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/RoleMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.Role; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | @Mapper 9 | public interface RoleMapper extends BaseMapper { 10 | 11 | 12 | Role loadUserRoleByUid(@Param("uid") String uid); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/SensitiveWordMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.SensitiveWord; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface SensitiveWordMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/TagMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.Tag; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | import java.util.List; 9 | 10 | @Mapper 11 | public interface TagMapper extends BaseMapper { 12 | 13 | List loadPostsIdByTagName(@Param("tagName") String tagName, @Param("start") Integer start, @Param("size") Integer size); 14 | 15 | Integer loadPostsCountByTagName(@Param("tagName") String tagName); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/TagOptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.TagOption; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface TagOptionMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.flip.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.flip.domain.entity.User; 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | @Mapper 9 | public interface UserMapper extends BaseMapper { 10 | 11 | int insertUserRole(@Param("uid") long uid, @Param("rid") int rid); 12 | 13 | void updateUserRole(@Param("uid") long uid, @Param("rid") int rid); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | import com.flip.common.Response; 4 | import com.flip.domain.entity.User; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | 7 | public interface AccountService { 8 | 9 | Boolean sendEmailCode(String to, String emailCode); 10 | 11 | Boolean register(User user, HttpServletRequest request, String captcha, String captchaOwner); 12 | 13 | Boolean checkUsernameUnique(String username); 14 | 15 | Response checkEmailUnique(String email); 16 | } 17 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/BannedUserService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.flip.domain.entity.BannedUser; 5 | 6 | public interface BannedUserService extends IService { 7 | 8 | void insertBannedHistory(Long uid, String createTime, String deadline, String reason); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.flip.domain.entity.Comment; 5 | 6 | import java.util.List; 7 | 8 | public interface CommentService extends IService { 9 | 10 | List getComments(String pid); 11 | 12 | Integer getRepliesNumOfComment(Integer commentId); 13 | 14 | List getReplies(String pid, Integer parentId); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/PostService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.flip.domain.entity.Post; 5 | import com.flip.domain.entity.PostTag; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public interface PostService extends IService { 11 | 12 | void savePostTag(PostTag postTag); 13 | 14 | Map getLatestPostList(Integer currentPage); 15 | 16 | Map getALlPostList(Integer page); 17 | 18 | Map getHotPostList(Integer page); 19 | 20 | void deletePostTags(Long pid); 21 | 22 | void addTagToPost(PostTag postTag); 23 | 24 | List getPostTags(String pid); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/SearchService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | public interface SearchService { 4 | } 5 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/SensitiveWordService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | 4 | import com.baomidou.mybatisplus.extension.service.IService; 5 | import com.flip.domain.entity.SensitiveWord; 6 | 7 | import java.util.List; 8 | 9 | public interface SensitiveWordService extends IService { 10 | 11 | List getSensitiveWords(); 12 | 13 | List getSensitiveStringWords(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/TagOptionService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.flip.domain.entity.TagOption; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public interface TagOptionService extends IService { 10 | 11 | List getAllTagOptions(); 12 | 13 | Map getTagsAndOptions(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/TagService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 4 | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; 5 | import com.baomidou.mybatisplus.core.metadata.IPage; 6 | import com.baomidou.mybatisplus.extension.service.IService; 7 | import com.flip.domain.entity.PostTag; 8 | import com.flip.domain.entity.Tag; 9 | 10 | import java.util.List; 11 | 12 | public interface TagService extends IService { 13 | 14 | List getAllTag(); 15 | 16 | IPage selectPostTagPage(IPage page, QueryWrapper queryWrapper); 17 | 18 | Boolean isTagUsed(Tag tag); 19 | 20 | void updatePostTag(UpdateWrapper updateWrapper); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.flip.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.flip.domain.dto.LoggedUser; 5 | import com.flip.domain.entity.Role; 6 | import com.flip.domain.entity.User; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | 9 | import java.util.List; 10 | 11 | public interface UserService extends IService{ 12 | 13 | User loadUserByUsername(String username); 14 | 15 | User loadUserByUid(Long uid); 16 | 17 | Role loadUserRoleByUid(String uid); 18 | 19 | List loadRoleAuthoritiesByRid(int rid); 20 | 21 | Boolean checkNicknameUnique(String nickname, LoggedUser loggedUser); 22 | 23 | Boolean correctPassword(BCryptPasswordEncoder passwordEncoder, LoggedUser loggedUser, String password); 24 | 25 | void updateUserRole(Long uid, Integer rid); 26 | } 27 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/BannedUserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import com.flip.domain.entity.BannedUser; 5 | import com.flip.mapper.BannedUserMapper; 6 | import com.flip.service.BannedUserService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class BannedUserServiceImpl extends ServiceImpl implements BannedUserService { 13 | 14 | private final BannedUserMapper bannedUserMapper; 15 | 16 | @Override 17 | public void insertBannedHistory(Long uid, String createTime, String deadline, String reason) { 18 | bannedUserMapper.insertBannedHistory(uid, createTime, deadline, reason); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/CommentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import com.flip.domain.entity.Comment; 5 | import com.flip.mapper.CommentMapper; 6 | import com.flip.service.CommentService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class CommentServiceImpl extends ServiceImpl implements CommentService { 15 | 16 | private final CommentMapper commentMapper; 17 | 18 | @Override 19 | public List getComments(String pid) { 20 | return commentMapper.getComments(pid); 21 | } 22 | 23 | @Override 24 | public Integer getRepliesNumOfComment(Integer commentId) { 25 | return commentMapper.getRepliesNumOfComment(commentId); 26 | } 27 | 28 | @Override 29 | public List getReplies(String pid, Integer parentId) { 30 | return commentMapper.getReplies(pid, parentId); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/SearchServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import com.flip.service.SearchService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | @RequiredArgsConstructor 9 | public class SearchServiceImpl implements SearchService { 10 | } 11 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/SensitiveWordServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.flip.domain.entity.SensitiveWord; 6 | import com.flip.mapper.SensitiveWordMapper; 7 | import com.flip.service.SensitiveWordService; 8 | import com.flip.utils.RedisKeyUtils; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.data.redis.core.RedisTemplate; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.List; 14 | 15 | @Service 16 | @RequiredArgsConstructor 17 | public class SensitiveWordServiceImpl extends ServiceImpl implements SensitiveWordService { 18 | 19 | private final String sensitiveObjsKey = RedisKeyUtils.getSensitiveObjsKey(); 20 | 21 | private final String sensitiveWordsKey = RedisKeyUtils.getSensitiveWordsKey(); 22 | 23 | private final SensitiveWordMapper sensitiveWordMapper; 24 | 25 | private final RedisTemplate redisTemplate; 26 | 27 | private final RedisTemplate stringRedisTemplate; 28 | 29 | @Override 30 | public List getSensitiveWords() { 31 | List sensitiveWords; 32 | 33 | Boolean hasKey = redisTemplate.hasKey(sensitiveObjsKey); 34 | if (Boolean.TRUE.equals(hasKey)) { 35 | sensitiveWords = redisTemplate.opsForList().range(sensitiveObjsKey, 0, -1); 36 | if (ObjectUtil.isNotNull(sensitiveWords)) { 37 | return sensitiveWords; 38 | } 39 | } 40 | 41 | sensitiveWords = sensitiveWordMapper.selectList(null); 42 | sensitiveWords.forEach(sensitiveWord -> { 43 | redisTemplate.opsForList().rightPush(sensitiveObjsKey, sensitiveWord); 44 | }); 45 | return sensitiveWords; 46 | } 47 | 48 | public List getSensitiveStringWords() { 49 | List sensitiveWords; 50 | Boolean hasKey = stringRedisTemplate.hasKey(sensitiveWordsKey); 51 | if (Boolean.TRUE.equals(hasKey)) { 52 | sensitiveWords = stringRedisTemplate.opsForList().range(sensitiveWordsKey, 0, -1); 53 | if (ObjectUtil.isNotNull(sensitiveWords)) { 54 | return sensitiveWords; 55 | } 56 | } 57 | 58 | getSensitiveWords().forEach(sensitiveWord -> { 59 | stringRedisTemplate.opsForList().rightPush(sensitiveWordsKey, sensitiveWord.getWord()); 60 | }); 61 | sensitiveWords = stringRedisTemplate.opsForList().range(sensitiveWordsKey, 0, -1); 62 | return sensitiveWords; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/TagOptionServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.flip.domain.entity.TagOption; 6 | import com.flip.mapper.TagOptionMapper; 7 | import com.flip.service.TagOptionService; 8 | import com.flip.service.TagService; 9 | import com.flip.utils.RedisKeyUtils; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | @Slf4j 19 | @Service 20 | @RequiredArgsConstructor 21 | public class TagOptionServiceImpl extends ServiceImpl implements TagOptionService { 22 | 23 | private final String tagOptionsKey = RedisKeyUtils.getTagOptionsKey(); 24 | 25 | private final TagOptionMapper tagOptionMapper; 26 | 27 | private final TagService tagService; 28 | 29 | private final RedisTemplate redisTemplate; 30 | 31 | @Override 32 | public List getAllTagOptions() { 33 | List tagOptions; 34 | Boolean hasKey = redisTemplate.hasKey(tagOptionsKey); 35 | if (Boolean.TRUE.equals(hasKey)) { 36 | tagOptions = redisTemplate.opsForList().range(tagOptionsKey, 0, -1); 37 | if (ObjectUtil.isNotNull(tagOptions)) { 38 | return tagOptions; 39 | } 40 | } 41 | 42 | tagOptions = tagOptionMapper.selectList(null); 43 | tagOptions.forEach(tagOption -> { 44 | redisTemplate.opsForList().rightPush(tagOptionsKey, tagOption); 45 | }); 46 | 47 | return tagOptions; 48 | } 49 | 50 | public Map getTagsAndOptions() { 51 | return Map.of("tags", tagService.getAllTag(), "tagOptions", getAllTagOptions()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/TagServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 5 | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; 6 | import com.baomidou.mybatisplus.core.metadata.IPage; 7 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 8 | import com.flip.domain.entity.PostTag; 9 | import com.flip.domain.entity.Tag; 10 | import com.flip.domain.entity.TagOption; 11 | import com.flip.mapper.PostTagMapper; 12 | import com.flip.mapper.TagMapper; 13 | import com.flip.mapper.TagOptionMapper; 14 | import com.flip.service.TagService; 15 | import com.flip.utils.RedisKeyUtils; 16 | import lombok.RequiredArgsConstructor; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.springframework.data.redis.core.RedisTemplate; 19 | import org.springframework.stereotype.Service; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | @Slf4j 25 | @Service 26 | @RequiredArgsConstructor 27 | public class TagServiceImpl extends ServiceImpl implements TagService { 28 | 29 | private final String tagsKey = RedisKeyUtils.getTagsKey(); 30 | 31 | private final TagMapper tagMapper; 32 | 33 | private final TagOptionMapper tagOptionMapper; 34 | 35 | private final PostTagMapper postTagMapper; 36 | 37 | private final RedisTemplate redisTemplate; 38 | 39 | @Override 40 | public List getAllTag() { 41 | List tags; 42 | Boolean hasKey = redisTemplate.hasKey(tagsKey); 43 | if (Boolean.TRUE.equals(hasKey)) { 44 | tags = redisTemplate.opsForList().range(tagsKey, 0, -1); 45 | if (ObjectUtil.isNotNull(tags)) { 46 | return tags; 47 | } 48 | } 49 | 50 | tags = tagMapper.selectList(null); 51 | List allTag = new ArrayList<>(); 52 | 53 | tags.forEach(tag -> { 54 | QueryWrapper tagOptionQueryWrapper = new QueryWrapper<>(); 55 | tagOptionQueryWrapper.eq("id", tag.getOptionId()); 56 | TagOption tagOption = tagOptionMapper.selectOne(tagOptionQueryWrapper); 57 | tag.setTagOption(tagOption); 58 | allTag.add(tag); 59 | redisTemplate.opsForList().rightPush(tagsKey, tag); 60 | }); 61 | return allTag; 62 | } 63 | 64 | @Override 65 | public IPage selectPostTagPage(IPage page, QueryWrapper queryWrapper) { 66 | return postTagMapper.selectPage(page, queryWrapper); 67 | } 68 | 69 | @Override 70 | public Boolean isTagUsed(Tag tag) { 71 | QueryWrapper postTagExistQueryWrapper = new QueryWrapper<>(); 72 | postTagExistQueryWrapper.eq("tag_label", tag.getLabel()); 73 | return postTagMapper.exists(postTagExistQueryWrapper); 74 | } 75 | 76 | @Override 77 | public void updatePostTag(UpdateWrapper updateWrapper) { 78 | postTagMapper.update(updateWrapper); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/UserDetailServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import com.flip.domain.dto.LoggedUser; 4 | import com.flip.domain.entity.User; 5 | import com.flip.service.UserService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | import org.springframework.stereotype.Service; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class UserDetailServiceImpl implements UserDetailsService { 15 | 16 | private final UserService userService; 17 | 18 | @Override 19 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 20 | User user = userService.loadUserByUsername(username); 21 | return new LoggedUser(user); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.flip.service.impl; 2 | 3 | import cn.hutool.core.util.ObjectUtil; 4 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 5 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 6 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 7 | import com.flip.domain.dto.LoggedUser; 8 | import com.flip.domain.entity.Authority; 9 | import com.flip.domain.entity.Role; 10 | import com.flip.domain.entity.User; 11 | import com.flip.mapper.AuthorityMapper; 12 | import com.flip.mapper.RoleMapper; 13 | import com.flip.mapper.UserMapper; 14 | import com.flip.service.UserService; 15 | import lombok.RequiredArgsConstructor; 16 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 17 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 18 | import org.springframework.stereotype.Service; 19 | import org.springframework.validation.annotation.Validated; 20 | 21 | import java.util.List; 22 | 23 | @Service 24 | @Validated 25 | @RequiredArgsConstructor 26 | public class UserServiceImpl extends ServiceImpl implements UserService { 27 | 28 | private final UserMapper userMapper; 29 | 30 | private final RoleMapper roleMapper; 31 | 32 | private final AuthorityMapper authorityMapper; 33 | 34 | @Override 35 | public void updateUserRole(Long uid, Integer rid) { 36 | userMapper.updateUserRole(uid, rid); 37 | } 38 | 39 | @Override 40 | public User loadUserByUsername(String username) { 41 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 42 | queryWrapper.eq(User::getUsername, username); 43 | User user = userMapper.selectOne(queryWrapper); 44 | if (ObjectUtil.isNull(user)) { 45 | throw new UsernameNotFoundException("请检查用户名是否输入正确"); 46 | } 47 | Role role = loadUserRoleByUid(String.valueOf(user.getUid())); 48 | user.setRole(role); 49 | 50 | List Authorities = loadRoleAuthoritiesByRid(user.getRole().getRid()); 51 | Authority authority = new Authority(); 52 | authority.setAuthorities(Authorities); 53 | user.setAuthority(authority); 54 | 55 | return user; 56 | } 57 | 58 | @Override 59 | public User loadUserByUid(Long uid) { 60 | return userMapper.selectById(uid); 61 | } 62 | 63 | @Override 64 | public Role loadUserRoleByUid(String uid) { 65 | return roleMapper.loadUserRoleByUid(uid); 66 | } 67 | 68 | @Override 69 | public List loadRoleAuthoritiesByRid(int rid) { 70 | return authorityMapper.loadRoleAuthoritiesByRid(rid); 71 | } 72 | 73 | @Override 74 | public Boolean checkNicknameUnique(String nickname, LoggedUser loggedUser) { 75 | QueryWrapper queryWrapper = new QueryWrapper<>(); 76 | queryWrapper.eq("nickname", nickname).or().eq("username", nickname); 77 | Long result = userMapper.selectCount(queryWrapper); 78 | if (result.intValue() == 0) { 79 | return true; 80 | } 81 | 82 | // 用户名与昵称不相同时,如果新昵称与用户名相同,则新昵称也可用。(相当于重置为默认昵称) 83 | return loggedUser.getUser().getUsername().equals(nickname); 84 | } 85 | 86 | @Override 87 | public Boolean correctPassword(BCryptPasswordEncoder passwordEncoder, LoggedUser loggedUser, String password) { 88 | return passwordEncoder.matches(password, loggedUser.getPassword()); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/AddressUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import cn.hutool.http.HttpUtil; 5 | import cn.hutool.json.JSONObject; 6 | import cn.hutool.json.JSONUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.nio.charset.StandardCharsets; 10 | 11 | /** 12 | * 根据IP获取真实地址 13 | */ 14 | @Slf4j 15 | public class AddressUtils { 16 | 17 | // IP地址查询 18 | public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; 19 | 20 | // 未知地址 21 | public static final String UNKNOWN = "XX XX"; 22 | 23 | public static String getRealAddressByIP(String ip) { 24 | if (IpUtils.internalIp(ip)) { 25 | return "内网IP"; 26 | } 27 | String rspStr = HttpUtil.get(IP_URL + "?ip=" + ip + "&json=true", StandardCharsets.UTF_8); 28 | if (StrUtil.isEmpty(rspStr)) { 29 | log.error("获取地理位置异常 {}", ip); 30 | return UNKNOWN; 31 | } 32 | JSONObject obj = JSONUtil.parseObj(rspStr); 33 | String province = obj.getStr("pro"); 34 | String city = obj.getStr("city"); 35 | if (StrUtil.isBlank(province) && StrUtil.isBlank(city)) { 36 | return obj.getStr("addr"); 37 | } 38 | return String.format("%s %s", province, city); 39 | } 40 | } -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/AvatarUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.http.HttpUtil; 5 | 6 | import java.io.File; 7 | 8 | public class AvatarUtils { 9 | 10 | public static String createAvatar(String avatarPath, String avatarPrefix, String avatarRemotePrefix, 11 | String avatarRemoteSuffix, String random, Long uid) { 12 | String remoteAvatar = avatarRemotePrefix + random + avatarRemoteSuffix; 13 | File localAvatarFile = FileUtil.file(avatarPath + uid); /* /Users/xxx/Desktop/Filp/avatar/UID */ 14 | if (!localAvatarFile.exists()) { 15 | boolean ready = localAvatarFile.mkdirs(); 16 | if (!ready) { 17 | return defaultAvatar(avatarPath, avatarPrefix, random, remoteAvatar); 18 | } 19 | } 20 | 21 | long size = HttpUtil.downloadFile(remoteAvatar, localAvatarFile); 22 | if (size > 0L) { 23 | File rawAvatarFile = FileUtil.file(localAvatarFile + "/" + random); /* /Users/xxx/Desktop/Filp/avatar/UID/Random */ 24 | String newAvatarName = System.currentTimeMillis() + ".png"; /* TIME.png */ 25 | FileUtil.rename(rawAvatarFile, newAvatarName, true); 26 | File newAvatarFile = FileUtil.file(localAvatarFile + "/" + newAvatarName); /* /Users/xxx/Desktop/Filp/avatar/UID/TIME.png */ 27 | if (newAvatarFile.exists()) { 28 | return avatarPrefix + "/avatar/" + uid + "/" + newAvatarName; /* http://localhost:8080/avatar/UID/TIME.png */ 29 | } else { 30 | return defaultAvatar(avatarPath, avatarPrefix, random, remoteAvatar); 31 | } 32 | } else { 33 | return defaultAvatar(avatarPath, avatarPrefix, random, remoteAvatar); 34 | } 35 | } 36 | 37 | public static String defaultAvatar(String avatarPath, String avatarPrefix, String random, String remoteAvatar) { 38 | File defaultAvatarPath = FileUtil.file(avatarPath + "default"); /* /Users/xxx/Desktop/Filp/avatar/default */ 39 | if (!defaultAvatarPath.exists()) { 40 | boolean ready = defaultAvatarPath.mkdirs(); 41 | if (!ready) return remoteAvatar; 42 | } else { 43 | File file = FileUtil.file(avatarPath + "default/default.png"); /* /Users/xxx/Desktop/Filp/avatar/default/default.png */ 44 | if (file.exists()) { 45 | return avatarPrefix + "/avatar/default/default.png"; /* http://localhost:8080/avatar/default/default.png */ 46 | } 47 | } 48 | 49 | long size = HttpUtil.downloadFile(remoteAvatar, defaultAvatarPath); 50 | if (size > 0L) { 51 | File rawAvatar = FileUtil.file(avatarPath + "default/" + random); /* /Users/xxx/Desktop/Filp/avatar/default/Random */ 52 | FileUtil.rename(rawAvatar, "default.png", true); 53 | return avatarPrefix + "/avatar/default/default.png"; /* http://localhost:8080/avatar/default/default.png */ 54 | } else return remoteAvatar; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/LoggedUserUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | import com.flip.domain.dto.LoggedUser; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | 6 | public class LoggedUserUtils { 7 | 8 | public static LoggedUser getLoggedUser() { 9 | return (LoggedUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/PageUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | import com.baomidou.mybatisplus.core.metadata.IPage; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class PageUtils { 11 | 12 | /** 13 | * 处理分页对象 14 | * @param page 分页对象 15 | * @param recordsKeyName 键值对集合中存放具体数据的键名 16 | * @return 键值对集合对象 17 | * @param 具体数据的类型 18 | */ 19 | public static Map handlePage(IPage page, String recordsKeyName) { 20 | HashMap map = new HashMap<>(); 21 | map.put("currentPage", page.getCurrent()); // 当前页码 22 | map.put("totalPageNum", page.getPages()); // 总页数 23 | map.put("sizePerPage", page.getSize()); // 每页数量大小 24 | map.put("totalRecordsNum", page.getTotal()); // 总记录数 25 | map.put(recordsKeyName, page.getRecords()); // 具体数据 26 | return map; 27 | } 28 | 29 | /** 30 | * 处理分页对象 31 | * @param page 分页对象 32 | * @param recordsKeyName 键值对集合中存放具体数据的键名 33 | * @param records 具体数据 34 | * @return 键值对集合对象 35 | * @param 具体数据的原类型 36 | * @param 具体数据的新类型 37 | */ 38 | public static Map handlePage(IPage page, String recordsKeyName, List records) { 39 | HashMap map = new HashMap<>(); 40 | map.put("currentPage", page.getCurrent()); // 当前页码 41 | map.put("totalPageNum", page.getPages()); // 总页数 42 | map.put("sizePerPage", page.getSize()); // 每页数量大小 43 | map.put("totalRecordsNum", page.getTotal()); // 总记录数 44 | map.put(recordsKeyName, records); // 具体数据 45 | return map; 46 | } 47 | 48 | /** 49 | * 获取分页对象中的具体数据集 50 | * @param map 存放了分页数据的键值对集合 51 | * @param recordsKeyName 具体数据的键名 52 | * @param recordClassType 具体数据的类型 53 | * @return 具体数据集合 54 | * @param 具体数据的类型 55 | */ 56 | public static List getRecords(Map map, String recordsKeyName, Class recordClassType) { 57 | if (map.containsKey(recordsKeyName)) { 58 | List out = new ArrayList<>(); 59 | Object objects = map.get(recordsKeyName); 60 | if (objects instanceof List) { 61 | ((List) objects).forEach(o -> out.add(recordClassType.cast(o))); 62 | return out; 63 | } 64 | } 65 | return new ArrayList<>(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/RedisKeyUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | /** 4 | * 专用于生产 Redis Key 的工具类 5 | */ 6 | public class RedisKeyUtils { 7 | 8 | private static final String SPLIT = ":"; 9 | private static final String PREFIX_CAPTCHA = "code:captcha"; 10 | private static final String PREFIX_ACTIVATE_EMAIL_CODE = "code:emailCode"; 11 | private static final String PREFIX_LOGGED_USER = "user:logged"; 12 | private static final String PREFIX_TAG_OPTIONS = "unit:tag:options"; 13 | private static final String PREFIX_TAGS = "unit:tag:tags"; 14 | private static final String PREFIX_SENSITIVE_OBJS = "unit:sensitive:objs"; 15 | private static final String PREFIX_SENSITIVE_WORDS = "unit:sensitive:words"; 16 | private static final String PREFIX_USER_NUMBER = "status:user:number"; 17 | private static final String PREFIX_POST_NUMBER = "status:post:number"; 18 | 19 | public static String getCaptchaKey(String captchaOwnerUUID) { 20 | return PREFIX_CAPTCHA + SPLIT + captchaOwnerUUID; 21 | } 22 | 23 | public static String getEmailCodeKey(String uid) { 24 | return PREFIX_ACTIVATE_EMAIL_CODE + SPLIT + uid + SPLIT + "code"; 25 | } 26 | 27 | public static String getEmailCodeRequestTimesKey(String uid) { 28 | return PREFIX_ACTIVATE_EMAIL_CODE + SPLIT + uid + SPLIT + "times"; 29 | } 30 | 31 | public static String getLoggedUserKey(String uid) { 32 | return PREFIX_LOGGED_USER + SPLIT + uid; 33 | } 34 | 35 | public static String getTagsKey() { 36 | return PREFIX_TAGS; 37 | } 38 | 39 | public static String getTagOptionsKey() { 40 | return PREFIX_TAG_OPTIONS; 41 | } 42 | 43 | public static String getSensitiveObjsKey() { 44 | return PREFIX_SENSITIVE_OBJS; 45 | } 46 | 47 | public static String getSensitiveWordsKey() { 48 | return PREFIX_SENSITIVE_WORDS; 49 | } 50 | 51 | public static String getUserNumberKey() { 52 | return PREFIX_USER_NUMBER; 53 | } 54 | 55 | public static String getPostNumberKey() { 56 | return PREFIX_POST_NUMBER; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/SensitiveWordUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | import toolgood.words.*; 4 | 5 | import java.util.List; 6 | 7 | public class SensitiveWordUtils { 8 | 9 | public static String wordsMatchExFilter(String text, List sensitiveWords) { 10 | WordsMatchEx wordsMatchEx = new WordsMatchEx(); 11 | try { 12 | wordsMatchEx.SetKeywords(sensitiveWords); 13 | } catch (Exception e) { 14 | throw new RuntimeException(e); 15 | } 16 | 17 | boolean contain = wordsMatchEx.ContainsAny(text); 18 | if (contain) { 19 | return wordsMatchEx.Replace(text, '*'); 20 | } 21 | 22 | return text; 23 | } 24 | 25 | public static String wordsSearchFilter(String text, List sensitiveWords) { 26 | WordsSearch wordsSearch = new WordsSearch(); 27 | wordsSearch.SetKeywords(sensitiveWords); 28 | 29 | boolean contain = wordsSearch.ContainsAny(text); 30 | if (contain) { 31 | return wordsSearch.Replace(text, '*'); 32 | } 33 | 34 | return text; 35 | } 36 | 37 | public static String wordsSearchExFilter(String text, List sensitiveWords) { 38 | WordsSearchEx wordsSearchEx = new WordsSearchEx(); 39 | wordsSearchEx.SetKeywords(sensitiveWords); 40 | 41 | boolean contain = wordsSearchEx.ContainsAny(text); 42 | if (contain) { 43 | return wordsSearchEx.Replace(text, '*'); 44 | } 45 | 46 | return text; 47 | } 48 | 49 | public static String wordsSearchEx2Filter(String text, List sensitiveWords) { 50 | WordsSearchEx2 wordsSearchEx2 = new WordsSearchEx2(); 51 | wordsSearchEx2.SetKeywords(sensitiveWords); 52 | 53 | boolean contain = wordsSearchEx2.ContainsAny(text); 54 | if (contain) { 55 | return wordsSearchEx2.Replace(text, '*'); 56 | } 57 | 58 | return text; 59 | } 60 | 61 | public static String stringSearchFilter(String text, List sensitiveWords) { 62 | StringSearch stringSearch = new StringSearch(); 63 | stringSearch.SetKeywords(sensitiveWords); 64 | 65 | boolean contain = stringSearch.ContainsAny(text); 66 | if (contain) { 67 | return stringSearch.Replace(text, '*'); 68 | } 69 | 70 | return text; 71 | } 72 | 73 | public static String stringSearchExFilter(String text, List sensitiveWords) { 74 | StringSearchEx stringSearchEx = new StringSearchEx(); 75 | stringSearchEx.SetKeywords(sensitiveWords); 76 | 77 | boolean contain = stringSearchEx.ContainsAny(text); 78 | if (contain) { 79 | return stringSearchEx.Replace(text, '*'); 80 | } 81 | 82 | return text; 83 | } 84 | 85 | public static String stringSearchEx2Filter(String text, List sensitiveWords) { 86 | StringSearchEx2 stringSearchEx2 = new StringSearchEx2(); 87 | stringSearchEx2.SetKeywords(sensitiveWords); 88 | 89 | boolean contain = stringSearchEx2.ContainsAny(text); 90 | if (contain) { 91 | return stringSearchEx2.Replace(text, '*'); 92 | } 93 | 94 | return text; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/SystemUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | import oshi.SystemInfo; 4 | import oshi.hardware.CentralProcessor; 5 | import oshi.hardware.GlobalMemory; 6 | import oshi.hardware.HardwareAbstractionLayer; 7 | import oshi.hardware.Sensors; 8 | import oshi.util.Util; 9 | 10 | import java.text.DecimalFormat; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class SystemUtils { 15 | 16 | private static final HardwareAbstractionLayer hardware; 17 | 18 | static { 19 | hardware = new SystemInfo().getHardware(); 20 | } 21 | 22 | public static Map getMemory() { 23 | GlobalMemory memory = hardware.getMemory(); 24 | long total = memory.getTotal(); 25 | long available = memory.getAvailable(); 26 | Map memoryMap = new HashMap<>(); 27 | memoryMap.put("total", formatByte(total)); 28 | memoryMap.put("available", formatByte(available)); 29 | return memoryMap; 30 | } 31 | 32 | public static Map getProcessor() { 33 | Sensors sensors = hardware.getSensors(); 34 | double cpuTemperature = sensors.getCpuTemperature(); 35 | 36 | CentralProcessor processor = hardware.getProcessor(); 37 | long[] preTicks = processor.getSystemCpuLoadTicks(); 38 | Util.sleep(1000); 39 | long[] ticks = processor.getSystemCpuLoadTicks(); 40 | long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - preTicks[CentralProcessor.TickType.NICE.getIndex()]; 41 | long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - preTicks[CentralProcessor.TickType.IRQ.getIndex()]; 42 | long softIrq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - preTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; 43 | long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - preTicks[CentralProcessor.TickType.STEAL.getIndex()]; 44 | long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - preTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; 45 | long user = ticks[CentralProcessor.TickType.USER.getIndex()] - preTicks[CentralProcessor.TickType.USER.getIndex()]; 46 | long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - preTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; 47 | long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - preTicks[CentralProcessor.TickType.IDLE.getIndex()]; 48 | long totalCpu = nice + irq + softIrq + steal + sys + user + ioWait + idle; 49 | 50 | Map cpuMap = new HashMap<>(); 51 | cpuMap.put("coreNum", processor.getLogicalProcessorCount()); 52 | cpuMap.put("idle", new DecimalFormat("#.##%").format(1.0 - (idle * 1.0 / totalCpu))); 53 | cpuMap.put("temperature", cpuTemperature); 54 | return cpuMap; 55 | } 56 | 57 | private static String formatByte(long byteNumber) { 58 | double FORMAT = 1024.0; 59 | double kbNumber = byteNumber / FORMAT; 60 | if (kbNumber < FORMAT) { 61 | return new DecimalFormat("#.##KB").format(kbNumber); 62 | } 63 | double mbNumber = kbNumber / FORMAT; 64 | if (mbNumber < FORMAT) { 65 | return new DecimalFormat("#.##MB").format(mbNumber); 66 | } 67 | double gbNumber = mbNumber / FORMAT; 68 | if (gbNumber < FORMAT) { 69 | return new DecimalFormat("#.##GB").format(gbNumber); 70 | } 71 | double tbNumber = gbNumber / FORMAT; 72 | return new DecimalFormat("#.##TB").format(tbNumber); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/TimeUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils; 2 | 3 | import java.util.Calendar; 4 | 5 | public class TimeUtils { 6 | 7 | public static Long getNowToNextDayMillis() { 8 | Calendar calendar = Calendar.getInstance(); 9 | calendar.add(Calendar.DAY_OF_YEAR, 1); 10 | calendar.set(Calendar.HOUR_OF_DAY, 0); 11 | calendar.set(Calendar.SECOND, 0); 12 | calendar.set(Calendar.MINUTE, 0); 13 | calendar.set(Calendar.MILLISECOND, 0); 14 | return (calendar.getTimeInMillis() - System.currentTimeMillis()); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/utils/elastic/ElasticUtils.java: -------------------------------------------------------------------------------- 1 | package com.flip.utils.elastic; 2 | 3 | import co.elastic.clients.elasticsearch.ElasticsearchClient; 4 | import co.elastic.clients.elasticsearch.core.GetResponse; 5 | import co.elastic.clients.elasticsearch.indices.DeleteIndexResponse; 6 | import co.elastic.clients.transport.endpoints.BooleanResponse; 7 | 8 | import java.io.IOException; 9 | 10 | public class ElasticUtils { 11 | public static boolean isIndexExist(ElasticsearchClient elasticsearchClient, String index) throws IOException { 12 | BooleanResponse response = elasticsearchClient.indices().exists(er -> er.index(index)); 13 | return response.value(); 14 | } 15 | 16 | public static boolean isDocumentExistInIndex(ElasticsearchClient elasticsearchClient, String documentId, String index) throws IOException { 17 | boolean indexExist = isIndexExist(elasticsearchClient, index); 18 | if (!indexExist) { 19 | return false; 20 | } 21 | GetResponse getResponse = elasticsearchClient.get(gr -> gr.index(index).id(documentId), Void.class); 22 | return getResponse.found(); 23 | } 24 | 25 | public static boolean deleteIndexInEs(ElasticsearchClient elasticsearchClient, String index) throws IOException { 26 | boolean indexExist = isIndexExist(elasticsearchClient, index); 27 | if (!indexExist) { 28 | return true; 29 | } 30 | 31 | DeleteIndexResponse deleteIndexResponse = elasticsearchClient.indices().delete(dir -> dir.index(index)); 32 | return deleteIndexResponse.acknowledged(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Flip-back/src/main/java/com/flip/validation/VG.java: -------------------------------------------------------------------------------- 1 | package com.flip.validation; 2 | 3 | import jakarta.validation.GroupSequence; 4 | 5 | /** 6 | * VG:ValidationGroup 7 | * 分组校验接口,用于定于校验顺序,只要有其中一个顺序的校验不通过,后续顺序的校验将会停止。 8 | * 此接口用在 @Validated 或 @Valid 中。(不配置的话,默认是随机顺序校验,并且会把全部都校验一遍,返回的结果可能是一大串) 9 | */ 10 | @GroupSequence({VG.First.class, VG.Second.class, VG.Third.class, VG.Fourth.class, VG.Fifth.class, 11 | VG.Sixth.class, VG.Seventh.class, VG.Eighth.class, VG.Ninth.class, VG.Tenth.class}) 12 | public interface VG { 13 | 14 | interface First {} 15 | 16 | interface Second {} 17 | 18 | interface Third {} 19 | 20 | interface Fourth {} 21 | 22 | interface Fifth {} 23 | 24 | interface Sixth {} 25 | 26 | interface Seventh {} 27 | 28 | interface Eighth {} 29 | 30 | interface Ninth {} 31 | 32 | interface Tenth {} 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: com.flip.FlipBackApplication 3 | 4 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/application-dev.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | data: 6 | redis: 7 | host: localhost 8 | port: 6379 9 | rabbitmq: 10 | host: localhost 11 | port: 5672 12 | username: guest 13 | password: guest 14 | virtual-host: /flip 15 | listener: 16 | simple: 17 | acknowledge-mode: manual #手动ACK 18 | mail: 19 | host: smtp.qq.com 20 | default-encoding: UTF-8 21 | username: abc@xx.com 22 | password: as********ebh 23 | properties: 24 | mail: 25 | smtp: 26 | auth: true #使用SMTP身份验证 27 | starttls: 28 | enable: true #开启SSL安全协议 29 | required: true 30 | 31 | mybatis-plus: 32 | configuration: 33 | map-underscore-to-camel-case: true 34 | #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印SQL日志 35 | 36 | elasticsearch: 37 | host: localhost 38 | port: 9200 39 | 40 | #关于CORS跨域的自定义属性 41 | cors: 42 | allowed-address: "http://localhost/" #允许跨域的域名地址 43 | 44 | #关于JWT的自定义属性 45 | jwt: 46 | token-header: Authorization 47 | token-prefix: "Bearer " 48 | secret: "TA61ja7A@a3yh4gz2trGtASt3hg2w47A15gtAok2@#fQEOQ1azGK" 49 | accessTokenTTL: 3600000 #accessToken过期时间,单位毫秒,3600000毫秒为两小时 50 | refreshTokenTTL: 604800000 #refreshToken过期时间,单位毫秒,604800000毫秒为7天 51 | auto-refresh-ttl: 120000 #凭证剩余时间小于该值时自动刷新,单位毫秒,1200000为二十分钟 52 | 53 | #关于邮件的自定义属性 54 | mail: 55 | from: abc@xx.com #发送邮件的来源 56 | register: 57 | subject: "请查收您的验证码" #注册邮件的主题 58 | 59 | #关于上传的自定义属性 60 | upload: 61 | avatarPath: "C:\\Users\\请修改此处为实际值\\Desktop\\Flip\\avatar\\" #头像真实存储路径 62 | staticPath: "C:\\Users\\请修改此处为实际值\\Desktop\\Flip\\static\\" #静态文件真实存储路径 63 | avatarMapperPath: "/avatar/" #头像的虚拟映射路径 64 | staticMapperPath: "/static/" #静态文件的虚拟映射路径 65 | 66 | avatar: 67 | prefix: 'http://localhost:8080' 68 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/application-prod.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | data: 6 | redis: 7 | host: localhost 8 | port: 6379 9 | rabbitmq: 10 | host: localhost 11 | port: 5672 12 | username: guest 13 | password: guest 14 | virtual-host: /flip 15 | listener: 16 | simple: 17 | acknowledge-mode: manual #手动ACK 18 | mail: 19 | host: smtp.qq.com 20 | default-encoding: UTF-8 21 | username: abc@xx.com 22 | password: as********ebh 23 | properties: 24 | mail: 25 | smtp: 26 | auth: true #使用SMTP身份验证 27 | starttls: 28 | enable: true #开启SSL安全协议 29 | required: true 30 | 31 | mybatis-plus: 32 | configuration: 33 | map-underscore-to-camel-case: true 34 | #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印SQL日志 35 | 36 | #关于CORS跨域的自定义属性 37 | cors: 38 | allowed-address: "https://xx.com/" #允许跨域的域名地址 39 | 40 | elasticsearch: 41 | host: localhost 42 | port: 9200 43 | 44 | #关于JWT的自定义属性 45 | jwt: 46 | token-header: Authorization 47 | token-prefix: "Bearer " 48 | secret: "TA61ja7A@a3yh4gz2trGtASt3hg2w47A15gtAok2@#fQEOQ1azGK" 49 | accessTokenTTL: 3600000 #accessToken过期时间,单位毫秒,3600000毫秒为两小时 50 | refreshTokenTTL: 604800000 #refreshToken过期时间,单位毫秒,604800000毫秒为7天 51 | auto-refresh-ttl: 120000 #凭证剩余时间小于该值时自动刷新,单位毫秒,1200000为二十分钟 52 | 53 | #关于邮件的自定义属性 54 | mail: 55 | from: abc@qq.com #发送邮件的来源 56 | register: 57 | subject: "请查收您的验证码" #注册邮件的主题 58 | 59 | #关于上传的自定义属性 60 | upload: 61 | avatarPath: "/www/wwwroot/flip/frontend/static/avatar/" #头像真实存储路径 62 | staticPath: "/www/wwwroot/flip/frontend/static/static/" #静态文件真实存储路径 63 | avatarMapperPath: "/avatar/" #头像的虚拟映射路径 64 | staticMapperPath: '/static/' #静态文件的虚拟映射路径 65 | 66 | avatar: 67 | prefix: "https://xx.com/static" 68 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: dev 4 | 5 | application: 6 | name: Flip 7 | datasource: 8 | type: com.zaxxer.hikari.HikariDataSource 9 | username: root 10 | password: root 11 | driver-class-name: com.mysql.cj.jdbc.Driver 12 | url: jdbc:mysql://localhost:3306/flip?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC 13 | servlet: 14 | multipart: 15 | enabled: true 16 | max-file-size: 3MB 17 | max-request-size: 100MB 18 | thymeleaf: 19 | mode: HTML 20 | prefix: classpath:/templates/ 21 | suffix: .html 22 | cache: false #关闭thymeleaf的页面缓存功能 23 | 24 | mybatis-plus: 25 | mapper-locations: classpath:/mapper/*.xml 26 | type-aliases-package: com.flip.domain.entity 27 | global-config: 28 | db-config: 29 | id-type: assign_id #默认是雪花算法生成ID,这里显式写出来了 30 | logic-delete-value: 1 #逻辑删除记录的值 31 | logic-not-delete-value: 0 #逻辑未删除记录的值 32 | logic-delete-field: deleted #逻辑删除对应的字段名 33 | 34 | avatar: 35 | remote-prefix: https://cravatar.cn/avatar/ 36 | remote-suffix: ?d=retro -------------------------------------------------------------------------------- /Flip-back/src/main/resources/mapper/AuthorityMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 13 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/mapper/BannedUserMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | INSERT INTO banned_history 8 | VALUES (null, #{uid}, #{createTime}, #{deadline}, #{reason}) 9 | 10 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/mapper/CommentMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 13 | 14 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/mapper/PostMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/mapper/RoleMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 13 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/mapper/TagMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | INSERT INTO post_tags 8 | VALUES (#{pid}, #{tagLabel}, #{tagName}, #{uid}) 9 | 10 | 11 | 16 | 17 | 18 | DELETE FROM post_tags WHERE pid = #{pid} 19 | 20 | 21 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | INSERT INTO user_role(uid, rid) VALUES (#{uid}, #{rid}) 8 | 9 | 10 | 11 | UPDATE user_role SET rid = #{rid} WHERE uid = #{uid} 12 | 13 | -------------------------------------------------------------------------------- /Flip-back/src/main/resources/templates/mail-register-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flip 6 | 105 | 106 | 107 | 108 | 109 | 110 | 142 | 143 | 144 |
111 |
112 | 113 | 114 |
115 |
116 |
117 |
118 | 尊敬的用户:您好! 119 | 120 | 您正在进行注册账号操作,请在验证码中输入以下验证码完成操作: 121 | 122 |
123 | 124 |
125 |
126 |
127 | 128 | 注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请及时登录并修改密码以保证帐户安全 129 |
(工作人员不会向你索取此验证码,请勿泄漏!) 130 |
131 |
132 |
133 |
134 |
135 |

此为系统邮件,请勿回复
136 | 请保管好您的邮箱,避免账号被他人盗用 137 |

138 |

——Romantik

139 |
140 |
141 |
145 | -------------------------------------------------------------------------------- /Flip-front/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /Flip-front/Flip-front.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Flip-front/env/.env: -------------------------------------------------------------------------------- 1 | #axios发送的超时时间,默认5000毫秒,即5秒 2 | VITE_AXIOS_TIMEOUT=5000 3 | 4 | #是否开启调试模式,开启后,会打印响应结果。{false:否, true: 是} 5 | VITE_DEBUG_MODE=false 6 | 7 | VITE_SYSTEM_CONTROL_URI='/sys-ctrl' -------------------------------------------------------------------------------- /Flip-front/env/.env.dev: -------------------------------------------------------------------------------- 1 | #axios发送的通用API前缀 2 | VITE_AXIOS_BASEURL='/api' 3 | 4 | #全局title 5 | VITE_GLOBAL_TITLE='Flip Dev' 6 | 7 | VITE_PROXY_TARGET='http://localhost:8080' 8 | 9 | VITE_LOGO_ADDRESS='http://localhost:8080/static/logo-32.png' -------------------------------------------------------------------------------- /Flip-front/env/.env.prod: -------------------------------------------------------------------------------- 1 | #axios发送的通用API前缀 2 | VITE_AXIOS_BASEURL='/api' 3 | 4 | #全局title 5 | VITE_GLOBAL_TITLE='Flip Prod' 6 | 7 | VITE_PROXY_TARGET='http://xx.com:8080/' 8 | 9 | VITE_LOGO_ADDRESS='https://xx.com/static/logo.png' -------------------------------------------------------------------------------- /Flip-front/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Flip-front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flip-front", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite --mode dev", 6 | "build": "vite build --mode prod", 7 | "preview": "vite preview --port 4173", 8 | "test:unit": "vitest --environment jsdom" 9 | }, 10 | "dependencies": { 11 | "@element-plus/icons-vue": "^2.0.10", 12 | "@fortawesome/fontawesome-svg-core": "^6.3.0", 13 | "@fortawesome/free-brands-svg-icons": "^6.3.0", 14 | "@fortawesome/free-regular-svg-icons": "^6.3.0", 15 | "@fortawesome/free-solid-svg-icons": "^6.3.0", 16 | "@fortawesome/vue-fontawesome": "^3.0.3", 17 | "@vueuse/core": "^9.6.0", 18 | "axios": "^1.1.3", 19 | "dayjs": "^1.11.6", 20 | "element-plus": "^2.2.18", 21 | "gsap": "^3.11.4", 22 | "js-cookie": "^3.0.1", 23 | "lodash": "^4.17.21", 24 | "moment": "^2.29.4", 25 | "nprogress": "^0.2.0", 26 | "pinia": "^2.0.23", 27 | "pinia-plugin-persistedstate": "^2.4.0", 28 | "undraw-ui": "^0.8.0", 29 | "vditor": "^3.8.18", 30 | "vue": "^3.2.38", 31 | "vue-axios": "^3.5.0", 32 | "vue-clipboard3": "^2.0.0", 33 | "vue-cropper": "^1.0.5", 34 | "vue-dompurify-html": "^3.1.2", 35 | "vue-router": "^4.1.5" 36 | }, 37 | "devDependencies": { 38 | "@vitejs/plugin-vue": "^3.0.3", 39 | "@vue/test-utils": "^2.0.2", 40 | "jsdom": "^20.0.0", 41 | "sass": "^1.56.1", 42 | "unplugin-auto-import": "^0.11.2", 43 | "unplugin-vue-components": "^0.22.8", 44 | "vite": "^3.0.9", 45 | "vitest": "^0.23.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Flip-front/public/assets/strawberry/fonts/StrawberryIcon-Free.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/assets/strawberry/fonts/StrawberryIcon-Free.eot -------------------------------------------------------------------------------- /Flip-front/public/assets/strawberry/fonts/StrawberryIcon-Free.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/assets/strawberry/fonts/StrawberryIcon-Free.ttf -------------------------------------------------------------------------------- /Flip-front/public/assets/strawberry/fonts/StrawberryIcon-Free.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/assets/strawberry/fonts/StrawberryIcon-Free.woff -------------------------------------------------------------------------------- /Flip-front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/favicon.ico -------------------------------------------------------------------------------- /Flip-front/public/images/admin_panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/admin_panel.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/avatar_change.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/avatar_change.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/comment_drawer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/comment_drawer.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/login.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/main.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/post.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/post.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/profile.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/replies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/replies.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/replies_black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/replies_black.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/search.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/search_black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/search_black.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/tag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/tag.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/tags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/tags.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/tags_black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/tags_black.jpg -------------------------------------------------------------------------------- /Flip-front/public/images/write.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshift/flip/da229f74fa4e45ceb1d1e476b90a2f853c6c5a5f/Flip-front/public/images/write.jpg -------------------------------------------------------------------------------- /Flip-front/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | 33 | 67 | -------------------------------------------------------------------------------- /Flip-front/src/api/admin/overviewAPI.js: -------------------------------------------------------------------------------- 1 | import request from "../../utils/request"; 2 | 3 | export function getSystemAPI() { 4 | return request({ 5 | url: '/systemInfo', 6 | method: 'get', 7 | headers: { 8 | requireToken: true, 9 | } 10 | }) 11 | } 12 | 13 | export function getForumAPI() { 14 | return request({ 15 | url: '/forumInfo', 16 | method: 'get', 17 | }) 18 | } -------------------------------------------------------------------------------- /Flip-front/src/api/admin/sensitiveAPI.js: -------------------------------------------------------------------------------- 1 | import request from "../../utils/request"; 2 | 3 | export function addSensitiveWordAPI(word) { 4 | const data = { 5 | word, 6 | } 7 | return request({ 8 | url: '/sys-ctrl/sensitiveWord', 9 | method: 'post', 10 | data, 11 | headers: { 12 | requireToken: true 13 | } 14 | }) 15 | } 16 | 17 | export function getSensitiveWordsAPI() { 18 | return request({ 19 | url: '/sys-ctrl/sensitiveWord', 20 | method: 'get', 21 | headers: { 22 | requireToken: true 23 | } 24 | }) 25 | } 26 | 27 | export function updateSensitiveWordAPI(sensitive) { 28 | const data = { 29 | id: sensitive.id, 30 | word: sensitive.word, 31 | } 32 | return request({ 33 | url: '/sys-ctrl/sensitiveWord', 34 | method: 'put', 35 | data, 36 | headers: { 37 | requireToken: true 38 | } 39 | }) 40 | } -------------------------------------------------------------------------------- /Flip-front/src/api/admin/tagAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export function getAllTagAPI() { 4 | return request({ 5 | url: '/tags', 6 | method: 'get', 7 | }) 8 | } 9 | 10 | export function getTagsAndOptionsAPI() { 11 | return request({ 12 | url: '/tagsAndOptions', 13 | method: 'get', 14 | }) 15 | } 16 | 17 | export function getTagAPI(tagLabel, currentPage) { 18 | return request({ 19 | url: '/tag', 20 | params: { 21 | label: tagLabel, 22 | page: currentPage, 23 | }, 24 | method: 'get', 25 | }) 26 | } 27 | 28 | export function addTagAPI(tagLabel, tagName, tagIcon, tagOption, tagDesc) { 29 | const data = { 30 | label: tagLabel, 31 | name: tagName, 32 | icon: tagIcon, 33 | optionId: tagOption, 34 | detail: tagDesc, 35 | }; 36 | return request({ 37 | url: '/sys-ctrl/tag', 38 | data, 39 | method: 'post', 40 | headers: { 41 | requireToken: true 42 | } 43 | }) 44 | } 45 | 46 | export function updateTagAPI(tag) { 47 | const data = { 48 | id: tag.id, 49 | label: tag.label, 50 | name: tag.name, 51 | icon: tag.icon, 52 | detail: tag.detail, 53 | optionId: tag.option, 54 | }; 55 | return request({ 56 | url: '/sys-ctrl/tag', 57 | data, 58 | method: 'put', 59 | headers: { 60 | requireToken: true 61 | } 62 | }) 63 | } 64 | 65 | export function deleteTagAPI(tag) { 66 | const data = { 67 | id: tag.id, 68 | label: tag.label, 69 | name: tag.name 70 | }; 71 | return request({ 72 | url: '/sys-ctrl/tag', 73 | data, 74 | method: 'delete', 75 | headers: { 76 | requireToken: true 77 | } 78 | }) 79 | } 80 | 81 | export function forceDeleteTagAPI(tag) { 82 | const data = { 83 | id: tag.id, 84 | label: tag.label, 85 | name: tag.name 86 | }; 87 | return request({ 88 | url: '/sys-ctrl/tagForce', 89 | data, 90 | method: 'delete', 91 | headers: { 92 | requireToken: true 93 | } 94 | }) 95 | } 96 | 97 | export function addTagOptionAPI(optionLabel, optionName) { 98 | const data = { 99 | label: optionLabel, 100 | name: optionName, 101 | } 102 | return request({ 103 | url: '/sys-ctrl/tagOption', 104 | data, 105 | method: 'post', 106 | headers: { 107 | requireToken: true, 108 | } 109 | }) 110 | } 111 | 112 | export function getAllTagOptionsAPI() { 113 | return request({ 114 | url: '/sys-ctrl/tagOption', 115 | method: 'get', 116 | }) 117 | } 118 | 119 | export function updateTagOptionAPI(tagOption) { 120 | const data = { 121 | id: tagOption.id, 122 | label: tagOption.label, 123 | name: tagOption.name, 124 | } 125 | return request({ 126 | url: '/sys-ctrl/tagOption', 127 | data, 128 | method: 'put', 129 | headers: { 130 | requireToken: true, 131 | } 132 | }) 133 | } 134 | 135 | export function deleteTagOptionAPI(tagOption) { 136 | const data = { 137 | id: tagOption.id, 138 | label: tagOption.label, 139 | name: tagOption.name, 140 | } 141 | return request({ 142 | url: '/sys-ctrl/tagOption', 143 | data, 144 | method: 'delete', 145 | headers: { 146 | requireToken: true, 147 | } 148 | }) 149 | } 150 | 151 | export function forceDeleteTagOptionAPI(tagOption) { 152 | const data = { 153 | id: tagOption.id, 154 | label: tagOption.label, 155 | name: tagOption.name, 156 | } 157 | return request({ 158 | url: '/sys-ctrl/tagOptionForce', 159 | data, 160 | method: 'delete', 161 | headers: { 162 | requireToken: true, 163 | } 164 | }) 165 | } -------------------------------------------------------------------------------- /Flip-front/src/api/admin/userSysAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export function getAllUserAPI() { 4 | return request({ 5 | url: '/sys-ctrl/users', 6 | method: 'get', 7 | headers: { 8 | requireToken: true 9 | } 10 | }) 11 | } 12 | 13 | export function bannedUserAPI(form) { 14 | const data = { 15 | uid: form.uid, 16 | deadline: form.datetime, 17 | reason: form.reason, 18 | } 19 | return request({ 20 | url: '/sys-ctrl/banUser', 21 | method: 'post', 22 | data, 23 | headers: { 24 | requireToken: true 25 | } 26 | }) 27 | } 28 | 29 | export function cancelBanUserAPI(uid) { 30 | return request({ 31 | url: '/sys-ctrl/banUser', 32 | method: 'delete', 33 | params: { 34 | uid, 35 | }, 36 | headers: { 37 | requireToken: true 38 | } 39 | }) 40 | } -------------------------------------------------------------------------------- /Flip-front/src/api/commentAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | /** 4 | * 根级评论 5 | * @param pid 评论的帖子ID 6 | * @param fromUid 评论者的UID 7 | * @param content 评论的内容 8 | * @returns {*} 9 | */ 10 | export function doCommentAPI(pid, fromUid, content) { 11 | const data = { 12 | pid: pid, 13 | fromUid: fromUid, 14 | content: content, 15 | } 16 | return request({ 17 | url: '/doComment', 18 | headers: { 19 | requireToken: true 20 | }, 21 | data: data, 22 | method: 'post' 23 | }) 24 | } 25 | 26 | /** 27 | * 楼中楼回复 28 | * @param pid 帖子ID 29 | * @param fromUid 回复者UID 30 | * @param toUid 被回复者UID 31 | * @param content 回复内容 32 | * @param parentId 父级ID 33 | * @param replyId 回复ID 34 | * @returns {*} 35 | */ 36 | export function doReplyAPI(pid, fromUid, toUid, content, parentId, replyId) { 37 | const data = { 38 | pid, 39 | fromUid, 40 | toUid, 41 | content, 42 | parentId, 43 | replyId, 44 | } 45 | return request({ 46 | url: '/doReply', 47 | headers: { 48 | requireToken: true 49 | }, 50 | data, 51 | method: 'post', 52 | }) 53 | } 54 | 55 | /** 56 | * 获取评论列表(不获取回复) 57 | * @param pid 帖子ID 58 | * @returns {*} 59 | */ 60 | export function getCommentsAPI(pid) { 61 | return request({ 62 | url: '/getComments', 63 | params: { 64 | pid: pid, 65 | }, 66 | method: 'get' 67 | }) 68 | } 69 | 70 | /** 71 | * 获取回复列表 72 | * @param pid 帖子ID 73 | * @param parentId 父级ID 74 | * @returns {*} 75 | */ 76 | export function getRepliesAPI(pid, parentId) { 77 | return request({ 78 | url: '/getReplies', 79 | params: { 80 | pid: pid, 81 | parentId: parentId 82 | }, 83 | method: 'get' 84 | }) 85 | } -------------------------------------------------------------------------------- /Flip-front/src/api/loginAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | /*注册方法*/ 4 | export function register(userInfo) { 5 | return request({ 6 | url: '/register?eCode=' + userInfo.emailVerificationCode + "&eOwner=" + userInfo.emailCodeOwner + "&code=" + userInfo.captcha + "&codeOwner=" + userInfo.captchaOwner, 7 | method: 'post', 8 | data: { 9 | username: userInfo.username.trim(), 10 | password: userInfo.password, 11 | email: userInfo.email 12 | } 13 | }) 14 | } 15 | 16 | /*登录方法*/ 17 | export function login(username, password, captcha, captchaOwner) { 18 | const data = { 19 | username: username, 20 | password: password 21 | } 22 | return request({ 23 | url: '/login?code=' + captcha + "&codeOwner=" + captchaOwner, 24 | method: 'post', 25 | data: data 26 | }) 27 | } 28 | 29 | export function refreshTokenAPI(refreshToken) { 30 | const data = { 31 | refreshToken: refreshToken, 32 | } 33 | return request({ 34 | url: '/refresh', 35 | data: data, 36 | method: 'post', 37 | headers: { 38 | requireToken: true, 39 | 'Content-Type': 'application/x-www-form-urlencoded' 40 | }, 41 | }) 42 | } 43 | 44 | /*退出登录方法*/ 45 | export function logout() { 46 | return request({ 47 | url: '/logout', 48 | headers: { 49 | requireToken: true 50 | }, 51 | method: 'post' 52 | }) 53 | } 54 | 55 | export function getEmailCodeAPI(uid) { 56 | return request({ 57 | url: '/activate', 58 | method: 'get', 59 | params: { 60 | uid, 61 | }, 62 | headers: { 63 | requireToken: true 64 | }, 65 | }) 66 | } 67 | 68 | export function activateEmailAPI(uid, emailCode) { 69 | return request({ 70 | url: '/activate', 71 | method: 'put', 72 | data: { 73 | uid, 74 | }, 75 | params: { 76 | emailCode, 77 | }, 78 | headers: { 79 | requireToken: true 80 | }, 81 | }) 82 | } 83 | 84 | /*获取图形验证码*/ 85 | export function getCaptchaImage() { 86 | return request({ 87 | url: '/_captcha', 88 | method: 'get', 89 | }) 90 | } 91 | 92 | /*校验图形验证码*/ 93 | export function checkCaptcha(code, codeOwner) { 94 | return request({ 95 | url: '/_checkCaptcha', 96 | params: { 97 | code: code, 98 | codeOwner: codeOwner 99 | }, 100 | headers: { 101 | 'Content-Type': 'application/x-www-form-urlencoded' 102 | }, 103 | method: 'get' 104 | }) 105 | } 106 | 107 | /*校验用户名唯一性*/ 108 | export function checkUsernameUnique(username) { 109 | return request({ 110 | url: '/_checkUsernameUnique', 111 | params: { 112 | username: username 113 | }, 114 | headers: { 115 | 'Content-Type': 'application/x-www-form-urlencoded' 116 | }, 117 | method: 'get' 118 | }) 119 | } 120 | 121 | -------------------------------------------------------------------------------- /Flip-front/src/api/postAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | /*发布帖子*/ 4 | export function doPost(title, content, tags) { 5 | const data = { 6 | title, 7 | content, 8 | tags, 9 | }; 10 | return request({ 11 | url: '/post', 12 | headers: { 13 | requireToken: true 14 | }, 15 | data: data, 16 | method: 'post' 17 | }) 18 | } 19 | 20 | /*修改帖子内容*/ 21 | export function doPostEditedContentAPI(pid, title, content) { 22 | const data = { 23 | id: pid, 24 | title: title, 25 | content: content 26 | }; 27 | return request({ 28 | url: '/postContent', 29 | headers: { 30 | requireToken: true 31 | }, 32 | data: data, 33 | method: 'put' 34 | }) 35 | } 36 | 37 | /*修改帖子标题*/ 38 | export function doPostEditedTitleAPI(pid, title) { 39 | return request({ 40 | url: '/postTitle', 41 | headers: { 42 | requireToken: true 43 | }, 44 | params: { 45 | pid: pid, 46 | title: title 47 | }, 48 | method: 'put' 49 | }) 50 | } 51 | 52 | export function doChangeTagOfPostAPI(pid, tags) { 53 | return request({ 54 | url: '/tagOfPost', 55 | data: { 56 | id: pid, 57 | tags, 58 | }, 59 | method: 'put', 60 | headers: { 61 | requireToken: true, 62 | } 63 | }) 64 | } 65 | 66 | /*帖子详情*/ 67 | export function getPostInfoAPI(pid) { 68 | return request({ 69 | url: '/postInfo', 70 | params: { 71 | pid: pid 72 | }, 73 | method: 'get' 74 | }) 75 | } 76 | 77 | /*获取最新主题列表*/ 78 | export function getLatestPostsAPI(currentPage) { 79 | return request({ 80 | url: '/list', 81 | headers: { 82 | requireToken: false 83 | }, 84 | params: { 85 | node: 'latestPost', 86 | page: currentPage, 87 | }, 88 | method: 'get' 89 | }) 90 | } 91 | 92 | /*获取全部主题*/ 93 | export function getAllPostsAPI(currentPage) { 94 | return request({ 95 | url: '/list', 96 | headers: { 97 | requireToken: false 98 | }, 99 | params: { 100 | node: 'allPost', 101 | page: currentPage, 102 | }, 103 | method: 'get', 104 | }) 105 | } 106 | 107 | /*获取热门主题*/ 108 | export function getHotPostsAPI(currentPage) { 109 | return request({ 110 | url: 'list', 111 | headers: { 112 | requireToken: false 113 | }, 114 | params: { 115 | node: 'hotPost', 116 | page: currentPage, 117 | }, 118 | method: 'get', 119 | }) 120 | } -------------------------------------------------------------------------------- /Flip-front/src/api/searchAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export function searchPostsAPI(keyword) { 4 | return request({ 5 | url: '/search/post', 6 | method: 'get', 7 | params: { 8 | keyword, 9 | } 10 | }) 11 | } 12 | 13 | export function searchUsersAPI(keyword) { 14 | return request({ 15 | url: '/search/user', 16 | method: 'get', 17 | params: { 18 | keyword, 19 | } 20 | }) 21 | } 22 | 23 | export function addPostsAPI() { 24 | return request({ 25 | url: '/search/addPosts', 26 | method: 'post', 27 | headers: { 28 | requireToken: true 29 | }, 30 | }) 31 | } 32 | 33 | export function addUsersAPI() { 34 | return request({ 35 | url: '/search/addUsers', 36 | method: 'post', 37 | headers: { 38 | requireToken: true 39 | }, 40 | }) 41 | } -------------------------------------------------------------------------------- /Flip-front/src/api/testAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export function testGetRoleAPI() { 4 | return request({ 5 | url: '/testRole', 6 | method: 'get', 7 | headers: { 8 | requireToken: true 9 | }, 10 | }) 11 | } 12 | 13 | export function testGetPermission() { 14 | return request({ 15 | url: '/getPermission', 16 | method: 'get', 17 | headers: { 18 | requireToken: true 19 | }, 20 | }) 21 | } -------------------------------------------------------------------------------- /Flip-front/src/api/uploadAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export function uploadAvatarAPI(action, data) { 4 | return request({ 5 | headers: { 6 | 'Content-Type': 'multipart/form-data', 7 | requireToken: true 8 | }, 9 | url: action, 10 | method: 'post', 11 | data: data 12 | }) 13 | } -------------------------------------------------------------------------------- /Flip-front/src/api/userAPI.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export function getProfileAPI() { 4 | return request({ 5 | url: '/profile', 6 | headers: { 7 | requireToken: true 8 | }, 9 | method: 'get' 10 | }) 11 | } 12 | 13 | export function getUserProfileAPI(username) { 14 | return request({ 15 | url: '/userProfile', 16 | params: { 17 | username, 18 | }, 19 | method: 'get' 20 | }) 21 | } 22 | 23 | export function getUserPostsAPI(username) { 24 | return request({ 25 | url: '/userPosts', 26 | params: { 27 | username, 28 | }, 29 | method: 'get' 30 | }) 31 | } 32 | 33 | export function updateNicknameAPI(nickname) { 34 | return request({ 35 | url: '/updateNickname', 36 | params: { 37 | nickname: nickname 38 | }, 39 | headers: { 40 | requireToken: true, 41 | }, 42 | method: 'get' 43 | }) 44 | } 45 | 46 | 47 | export function updatePasswordAPI(currentPassword, newPassword, confirmNewPassword) { 48 | return request({ 49 | url: '/updatePassword', 50 | data: { 51 | currentPassword: currentPassword, 52 | newPassword: newPassword, 53 | confirmNewPassword: confirmNewPassword, 54 | }, 55 | headers: { 56 | requireToken: true, 57 | 'Content-Type': 'application/x-www-form-urlencoded', 58 | }, 59 | method: 'post' 60 | }) 61 | } -------------------------------------------------------------------------------- /Flip-front/src/assets/styles/dark/css-vars.css: -------------------------------------------------------------------------------- 1 | html { 2 | --el-bg-color: #f5f7fa !important; /*主背景色*/ 3 | --el-menu-active-color: #3366ff !important; /*菜单激活状态颜色*/ 4 | --el-menu-item-font-size: 15px !important; /*Header菜单文字大小*/ 5 | 6 | --custom-tabs-color: #606266; 7 | 8 | --custom-border-color: #d5d7de; /*自定义边框颜色*/ 9 | --custom-header-bg-color: white; /*头部背景颜色*/ 10 | --custom-footer-bg-color: white; /*底部背景颜色*/ 11 | 12 | --custom-trend-header-bg-color: #D4D7DE; /*主体trend头部背景颜色,如首页节点处颜色和浏览帖子处内容头部背景颜色*/ 13 | --custom-trend-header-color: #696969;/*主页trend头部文字颜色*/ 14 | 15 | --custom-text-color: #222; /*正文文字颜色,如帖子标题、帖子内容等处的颜色*/ 16 | --custom-text-bg-color: white; /*正文背景颜色*/ 17 | 18 | --custom-divider-border-color: #dcdfe6; /*分割线边框颜色*/ 19 | 20 | --custom-trend-td-bottom-color: #eaeaea; /*主页trend处帖子列表底部边框颜色*/ 21 | --custom-trend-tr-color: #333; /*主页trend处帖子列表集tr的文字颜色*/ 22 | --custom-trend-tr-time-color: #999; /*主页trend处帖子列表集tr中的时间的文字颜色*/ 23 | 24 | --custom-breadcrumb-color: #808080 !important; /*面包屑字体颜色*/ 25 | 26 | --custom-reply-num-bg-color: #dae6ef; /*Main列表中帖子回复数背景颜色*/ 27 | --custom-reply-num-color: #626f84; /*Main列表中帖子回复数文字颜色*/ 28 | --custom-view-num-bg-color: #e8edf2; /*Main列表中帖子查看数背景颜色*/ 29 | --custom-view-num-color: #999; /*Main列表中帖子查看数文字颜色*/ 30 | 31 | --custom-post-header-details-color: #808080; /*查看/回复/感谢文字颜色*/ 32 | 33 | --custom-border-radius: 0.4rem; /*自定义全局border radius*/ 34 | 35 | --custom-card-bg-color: #fcfdfd; /*卡片背景颜色*/ 36 | 37 | --custom-aside-menu-bg-color: white; /*Aside背景颜色*/ 38 | 39 | --custom-aside-box-shadow: 0 0 3px rgb(18 18 18 / 10%); /*阴影*/ 40 | 41 | --custom-tabs-border-color: #ebebeb; 42 | } 43 | 44 | 45 | html.dark { 46 | --el-bg-color: #333840 !important; /* 主背景色 */ 47 | --el-menu-border-color: #323840 !important; /* 顶部底部上下边框颜色 */ 48 | --el-bg-color-overlay: #2a3039 !important; /*主页帖子列表table背景颜色*/ 49 | --el-menu-text-color: #f5f5f5 !important; /*头部Menu文字颜色*/ 50 | 51 | --custom-tabs-color: #caced7; 52 | 53 | --custom-border-color: #3f444e; /*边框颜色*/ 54 | --custom-header-bg-color: #232830; /*Header背景颜色*/ 55 | --custom-footer-bg-color: #232830; /*Footer背景颜色*/ 56 | 57 | --custom-trend-header-bg-color: #232830; /*主页trend头部背景颜色,如首页节点处背景颜色和浏览帖子处内容头部背景颜色*/ 58 | --custom-trend-header-color: #f5f5f5; /*主页trend头部文字颜色*/ 59 | 60 | --custom-text-bg-color: #2a3039; /*正文背景颜色*/ 61 | --custom-text-color: #E4E7ED; /*正文文字颜色,如帖子标题、帖子内容等处的颜色*/ 62 | 63 | --custom-divider-border-color: #474c57; /*分割线边框颜色*/ 64 | 65 | --custom-trend-td-bottom-color: #333942; /*主页帖子列表中每个帖子的底部的边框颜色*/ 66 | --custom-trend-tr-color: #aab2bc; /*主页帖子列表集tr的文字颜色*/ 67 | --custom-trend-tr-time-color: #aab2bc; /*主页帖子列表集tr中的时间的文字颜色*/ 68 | 69 | --custom-breadcrumb-color: #f5f5f5 !important; /*面包屑字体颜色*/ 70 | 71 | --custom-reply-num-bg-color: #6c737e; /*Main列表中帖子回复数背景颜色*/ 72 | --custom-reply-num-color: #e8edf2; /*Main列表中帖子回复查看数文字颜色*/ 73 | --custom-view-num-bg-color: #4c5159; /*Main列表中帖子查看数背景颜色*/ 74 | --custom-view-num-color: #e8edf2; /*Main列表中帖子查看数文字颜色*/ 75 | 76 | --custom-post-header-details-color: #999; /*查看/回复/感谢文字颜色*/ 77 | 78 | --custom-avatar-border-color: #bdaf7f; /*头像边框颜色,边框颜色尽显暗色模式*/ 79 | 80 | --custom-card-bg-color: #2a3039; /*卡片背景颜色*/ 81 | 82 | --custom-aside-menu-bg-color: #333840; /*Aside背景颜色*/ 83 | 84 | --custom-tabs-border-color: transparent; 85 | } -------------------------------------------------------------------------------- /Flip-front/src/components/layout/aside/index/CountdownAside.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 31 | 32 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/aside/index/HotTagAside.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 50 | 51 | 60 | 61 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/aside/index/StatisticsAside.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/aside/post/PostAuthorAside.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 57 | 58 | 125 | 126 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/dialog/DevelopingDialog.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 41 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/dialog/NoLoginDialog.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 46 | 47 | 50 | 51 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/dialog/NoVerifyEmailDialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/footer/Footer.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/header/expand/LeftExpandMenu.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | 31 | 34 | 35 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/header/search/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | 34 | 40 | 41 | -------------------------------------------------------------------------------- /Flip-front/src/components/layout/header/togger/ThemeToggle.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | -------------------------------------------------------------------------------- /Flip-front/src/components/page/CascadePage.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | 35 | 86 | 87 | -------------------------------------------------------------------------------- /Flip-front/src/components/search/SearchedPosts.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 68 | 69 | 146 | 147 | -------------------------------------------------------------------------------- /Flip-front/src/components/user/profile/UserBookmarks.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /Flip-front/src/components/user/profile/UserComments.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /Flip-front/src/components/user/profile/UserFollows.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /Flip-front/src/components/user/profile/UserPosts.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 65 | 66 | 112 | 113 | -------------------------------------------------------------------------------- /Flip-front/src/directive/authority/hasAuthority.js: -------------------------------------------------------------------------------- 1 | import useUserStore from "../../stores/userStore"; 2 | 3 | /** 4 | * https://cn.vuejs.org/guide/reusability/custom-directives.html 5 | */ 6 | export default { 7 | mounted(el, binding, vnode, prevVnode) { 8 | const {value} = binding; //解构出来的 value 是传递给指令的值。例如 v-hasAuthority="activity:post:write" 中,value 值是 activity:post:write。 9 | const authorities = useUserStore().authorities; 10 | 11 | if (value && value instanceof Array && value.length > 0) { 12 | const authorityValue = value; 13 | 14 | const hasAuthority = authorities.some(authority => { 15 | return authorityValue.includes(authority.authority) 16 | }) 17 | 18 | if (!hasAuthority) { 19 | el.parentNode && el.parentNode.removeChild(el); //el是指令绑定的元素 20 | } 21 | } else throw new Error('需要权限信息') 22 | } 23 | } -------------------------------------------------------------------------------- /Flip-front/src/directive/authority/hasRole.js: -------------------------------------------------------------------------------- 1 | import useUserStore from "../../stores/userStore"; 2 | 3 | /** 4 | * https://cn.vuejs.org/guide/reusability/custom-directives.html 5 | * 6 | */ 7 | export default { 8 | mounted(el, binding, vnode, prevVnode) { 9 | const {value} = binding; //解构出来的 value 是传递给指令的值。例如 v-hasRole="ROLE_ADMIN" 中,value 值是 ROLE_ADMIN。 10 | const roles = useUserStore().role; 11 | console.log(roles) 12 | 13 | if (value && value instanceof Array && value.length > 0) { 14 | const roleValue = value; 15 | 16 | const hasRole = roles.some(role => { 17 | return roleValue.includes(role) 18 | }) 19 | 20 | if (!hasRole) { 21 | el.parentNode && el.parentNode.removeChild(el) 22 | } 23 | } else throw new Error('需要角色信息') 24 | } 25 | } -------------------------------------------------------------------------------- /Flip-front/src/directive/index.js: -------------------------------------------------------------------------------- 1 | import hasRole from './authority/hasRole'; 2 | import hasAuthority from './authority/hasAuthority'; 3 | 4 | /** 5 | * 自定义指定:https://cn.vuejs.org/guide/reusability/custom-directives.html 6 | * @param app createApp 7 | */ 8 | export default function directive(app) { 9 | app.directive('hasRole', hasRole); // v-hasRole 10 | app.directive('hasAuthority', hasAuthority); // v-hasAuthority 11 | } -------------------------------------------------------------------------------- /Flip-front/src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue'; 2 | import {createPinia} from 'pinia'; 3 | 4 | import App from './App.vue'; 5 | import ElementPlus from 'element-plus'; 6 | import router from '@/router/index'; 7 | import 'element-plus/dist/index.css'; 8 | import * as ElementPlusIconsVue from '@element-plus/icons-vue'; 9 | import axios from 'axios'; 10 | import VueAxios from 'vue-axios'; 11 | import _ from 'lodash'; 12 | import dayjs from 'dayjs'; 13 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; 14 | import 'element-plus/theme-chalk/dark/css-vars.css'; 15 | import VueCropper from 'vue-cropper'; 16 | import 'vue-cropper/dist/index.css'; 17 | import gsap from 'gsap'; 18 | import {ScrollTrigger} from 'gsap/ScrollTrigger'; 19 | import {ScrollToPlugin} from 'gsap/ScrollToPlugin'; 20 | import './assets/styles/dark/css-vars.css'; /*引入自定义css变量文件*/ 21 | import VueDOMPurifyHTML from 'vue-dompurify-html'; 22 | import moment from "moment"; 23 | import 'moment/dist/locale/zh-cn'; 24 | import UndrawUi from 'undraw-ui'; 25 | import 'undraw-ui/dist/style.css'; 26 | import directive from './directive'; 27 | import { library } from '@fortawesome/fontawesome-svg-core'; 28 | import { } from '@fortawesome/free-brands-svg-icons'; 29 | import { faLightbulb } from '@fortawesome/free-regular-svg-icons'; 30 | import { faCar, faMotorcycle, faBicycle, faTractor, faGavel, faStreetView, faSignHanging, faRoadCircleExclamation, 31 | faPersonWalkingDashedLineArrowRight, faCarBurst, faPersonBiking, faVanShuttle, faPersonFallingBurst, faShieldHalved, faScaleBalanced, 32 | faHardDrive, faBacon, faRoad, faMugHot } from '@fortawesome/free-solid-svg-icons'; 33 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; 34 | /*import { faVuejs, faJava, faReact, faCss3Alt, faSquareJs, faSlideshare, faConnectdevelop } from '@fortawesome/free-brands-svg-icons'; 35 | import { faFeather, faClipboardQuestion, faFlag, faBug, faEarthAsia, faGamepad, faFilm, faAddressCard, faCodeCompare, 36 | faVideo, faBriefcase, faAward, faSquareShareNodes, faBuildingUser, faFolderTree, faListCheck } from '@fortawesome/free-solid-svg-icons';*/ 37 | 38 | const app = createApp(App); 39 | const pinia = createPinia(); 40 | 41 | gsap.registerPlugin(ScrollTrigger, ScrollToPlugin); 42 | 43 | /*library.add(faVuejs, faJava, faReact, faCss3Alt, faSquareJs, faFeather, faSlideshare, faClipboardQuestion, 44 | faFlag, faBug, faEarthAsia, faGamepad, faFilm, faVideo, faBriefcase, faAward, faSquareShareNodes, 45 | faBuildingUser, faAddressCard, faCodeCompare, faConnectdevelop, faFolderTree, faListCheck)*/ 46 | library.add(faCar, faMotorcycle, faBicycle, faTractor, faLightbulb, faGavel, faStreetView, faSignHanging, faRoadCircleExclamation, 47 | faPersonWalkingDashedLineArrowRight, faCarBurst, faPersonBiking, faVanShuttle, faPersonFallingBurst, faShieldHalved, faScaleBalanced, 48 | faHardDrive, faBacon, faRoad, faMugHot) 49 | app.component('font-awesome-icon', FontAwesomeIcon) 50 | 51 | /* 全局导入Element图标 */ 52 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 53 | app.component(key, component); 54 | } 55 | 56 | pinia.use(piniaPluginPersistedstate); 57 | 58 | moment.locale('zh-CN'); 59 | 60 | /* 挂载自定义指令 */ 61 | directive(app); 62 | 63 | app.use(router); 64 | app.use(pinia); 65 | app.use(VueAxios, axios); 66 | app.use(ElementPlus); 67 | app.use(VueDOMPurifyHTML); 68 | app.use(VueCropper); 69 | app.use(_); 70 | app.use(UndrawUi) 71 | app.use(dayjs); 72 | app.use(moment); 73 | 74 | app.mount('#app'); 75 | -------------------------------------------------------------------------------- /Flip-front/src/stores/tabStore.js: -------------------------------------------------------------------------------- 1 | import {defineStore} from "pinia"; 2 | 3 | const tabPrefix = '/n/'; 4 | 5 | const useTabStore = defineStore("tab", { 6 | state: () => ({ 7 | mainTabs: [ 8 | { name: "全部", path: "/", }, 9 | { name: "最新", path: tabPrefix + "latest", }, 10 | { name: "热门", path: tabPrefix + "hot", }, 11 | { name: "标签", path: '/tags', icon: "czs-block-l" }, 12 | ], 13 | menuTabs: [ 14 | { name: "标签", path: '/tags', icon: "czs-block-l" }, 15 | /*{ name: "广场", path: '/plaza', icon: "czs-shop-l" },*/ 16 | ], 17 | userTabs: [ 18 | { name: "主题", path: '' }, 19 | { name: "评论", path: 'comments' }, 20 | { name: "关注", path: 'follow' }, 21 | { name: "收藏", path: 'bookmark' }, 22 | ], 23 | defaultAsideMainMenus: [ 24 | { name: "首页", path: "/", icon: "czs-home-l" }, 25 | { name: "博客", path: "/blog", icon: "czs-book-l" }, 26 | { name: "广场", path: "/plaza", icon: "czs-shop-l" }, 27 | { name: "标签", path: "/tags", icon: "czs-block-l" } 28 | ], 29 | defaultAsideMinorMenus: [ 30 | { name: "聊天室", path: "/chat", icon: "czs-talk-l" }, 31 | { name: "朋友圈", path: "/moments", icon: "czs-moments" }, 32 | ], 33 | }) 34 | }) 35 | 36 | export default useTabStore -------------------------------------------------------------------------------- /Flip-front/src/stores/themeStore.js: -------------------------------------------------------------------------------- 1 | import {defineStore} from "pinia/dist/pinia"; 2 | 3 | const useThemeStore = defineStore('theme', { 4 | state: () => ({ 5 | currentTheme: '', 6 | hljsTheme: '', 7 | }), 8 | }) 9 | 10 | export default useThemeStore -------------------------------------------------------------------------------- /Flip-front/src/stores/userStore.js: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia' 2 | import {removeToken, setRefreshToken, setToken} from "@/utils/token"; 3 | import {login, logout} from "../api/loginAPI"; 4 | import {getProfileAPI} from "../api/userAPI"; 5 | 6 | const useUserStore = defineStore("user", { 7 | state: () => ({ 8 | token: '', 9 | refreshToken: '', 10 | id: '', 11 | uid: '', 12 | username: '', 13 | nickname: '', 14 | email: '', 15 | emailVerified: Boolean, 16 | avatar: '', 17 | createTime: '', 18 | registerIp: '', 19 | role: [], 20 | authorities: [], 21 | isLogin: false, 22 | }), 23 | actions: { 24 | /*用户登录*/ 25 | login(userInfo) { 26 | const username = userInfo.username.trim(); 27 | const password = userInfo.password; 28 | const captcha = userInfo.captcha; 29 | const captchaOwner = userInfo.captchaOwner; 30 | return new Promise((resolve, reject) => { 31 | login(username, password, captcha, captchaOwner).then(response => { 32 | setToken(response.data.token); 33 | setRefreshToken(response.data.refreshToken) 34 | this.token = response.data.token; 35 | this.refreshToken = response.data.refreshToken; 36 | resolve(response); 37 | }).catch(error => { 38 | reject(error); 39 | }) 40 | }) 41 | }, 42 | /*获取用户信息*/ 43 | getProfile() { 44 | return new Promise((resolve, reject) => { 45 | getProfileAPI().then(response => { 46 | const user = response.data; 47 | this.$patch({ 48 | isLogin: true, 49 | id: user.id, 50 | uid: user.uid, 51 | username: user.username, 52 | nickname: user.nickname, 53 | email: user.email, 54 | emailVerified: user.emailVerified, 55 | avatar: user.avatar, 56 | createTime: user.createTime, 57 | registerIp: user.registerIp, 58 | }) 59 | 60 | if (user.role) { 61 | this.role.push(user.role); 62 | } else { 63 | this.role = ['ROLE_USER'] 64 | } 65 | if (user.authorities && user.authorities.length > 0) { 66 | this.authorities = user.authorities; 67 | } else { 68 | this.authorities = null; 69 | } 70 | resolve(response); 71 | }).catch(error => { 72 | reject(error); 73 | }) 74 | }) 75 | }, 76 | /*退出登录*/ 77 | logout() { 78 | return new Promise((resolve, reject) => { 79 | logout().then(response => { 80 | this.$reset(); // 批量重置state 81 | removeToken(); 82 | resolve(response); 83 | }).catch(error => { 84 | this.$reset(); // 批量重置state 85 | removeToken(); 86 | reject(error) 87 | }) 88 | }) 89 | }, 90 | getLoggedUserInfo() { 91 | return { 92 | id: this.id, 93 | uid: this.uid, 94 | username: this.username, 95 | nickname: this.nickname, 96 | email: this.email, 97 | emailVerified: this.emailVerified, 98 | avatar: this.avatar, 99 | createTime: this.createTime, 100 | registerIp: this.registerIp, 101 | role: this.role, 102 | }; 103 | } 104 | }, 105 | }) 106 | 107 | export default useUserStore -------------------------------------------------------------------------------- /Flip-front/src/utils/errorMsg.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '401': '登录已过期', //accessToken过期引起的异常 3 | '403': '权限不足', 4 | '404': '404 Not Found', 5 | '409': '状态过期,请重新登录', //双token过期引起的异常 6 | '412': '状态异常,请重新登录', //任一Token格式不对引起的异常 7 | '500': '服务器异常', 8 | 'default': '发生未知错误' 9 | } -------------------------------------------------------------------------------- /Flip-front/src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import useUserStore from "../stores/userStore"; 2 | 3 | export function hasRole(roleValue) { 4 | if (roleValue && roleValue instanceof Array && roleValue.length > 0) { 5 | const roles = useUserStore().role; 6 | 7 | return roles.some(role => { 8 | return roleValue.includes(role) 9 | }); 10 | } else return false; 11 | } 12 | 13 | export function hasAuthority(authorityValue) { 14 | if (authorityValue && authorityValue instanceof Array && authorityValue.length > 0) { 15 | const authorities = useUserStore().authorities; 16 | 17 | if (authorities === null) { 18 | return false; 19 | } 20 | 21 | return authorities.some(authority => { 22 | return authorityValue.includes(authority.authority); 23 | }) 24 | } else return false; 25 | } -------------------------------------------------------------------------------- /Flip-front/src/utils/tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 转换标签,用于在tags页面展示 3 | * @param tags 4 | * @param tagOptions 5 | * @returns {{}} 6 | */ 7 | export function classifyTags(tags, tagOptions) { 8 | let allTag = JSON.parse(JSON.stringify(tags)); 9 | let result = {}; 10 | 11 | tagOptions.forEach(tagOption => { 12 | if (!result.hasOwnProperty(tagOption.label)) { 13 | result[tagOption.label] = [] 14 | } 15 | }); 16 | 17 | allTag.forEach(tag => { 18 | result[tag.tagOption.label].push(tag) 19 | }) 20 | 21 | return result; 22 | } 23 | 24 | /** 25 | * 让「其它」类型永远排在最后面,用于在tags页面展示 26 | * @param tagOptions 27 | */ 28 | export function classifyTagOptions(tagOptions) { 29 | let result = []; 30 | let other = {}; 31 | let isOther = false; 32 | 33 | tagOptions.forEach(tagOption => { 34 | isOther = false; 35 | if (tagOption.label === 'other') { 36 | isOther = true; 37 | other = tagOption; 38 | } 39 | if (!isOther) { 40 | result.push(tagOption); 41 | } 42 | }) 43 | result.push(other); 44 | return result; 45 | } 46 | 47 | /** 48 | * 级联标签,用于el-cascader组件中展示 49 | * @param tags 50 | * @param tagOptions 51 | */ 52 | export function cascadeTags(tags, tagOptions) { 53 | let allTag = JSON.parse(JSON.stringify(tags)); 54 | let result = []; 55 | 56 | tagOptions.forEach(tagOption => { 57 | if (!result[tagOption.label]) { 58 | result.push({ 59 | value: tagOption.label, 60 | label: tagOption.name, 61 | children: [], 62 | }) 63 | } 64 | }) 65 | 66 | allTag.forEach(tag => { 67 | result.forEach(option => { 68 | if (option.value === tag.tagOption.label) { 69 | option.children.push({ 70 | value: tag.label, 71 | label: tag.name, 72 | }); 73 | } 74 | }) 75 | }) 76 | 77 | return result; 78 | } -------------------------------------------------------------------------------- /Flip-front/src/utils/token.js: -------------------------------------------------------------------------------- 1 | const tokenKey = 'token'; 2 | const refreshTokenKey = 'refreshToken'; 3 | 4 | export function tokenExists() { 5 | if (getToken() === null && getRefreshToken() !== null) { 6 | removeToken(); 7 | return false; 8 | } else if (getToken() !== null && getRefreshToken() === null) { 9 | removeToken(); 10 | return false; 11 | } else return !(getToken() === null && getRefreshToken() === null); 12 | } 13 | 14 | export function getToken() { 15 | return localStorage.getItem(tokenKey) 16 | } 17 | export function getRefreshToken() { 18 | return localStorage.getItem(refreshTokenKey) 19 | } 20 | 21 | export function setToken(token) { 22 | localStorage.setItem(tokenKey, token) 23 | } 24 | export function setRefreshToken(refreshToken) { 25 | localStorage.setItem(refreshTokenKey, refreshToken) 26 | } 27 | 28 | export function removeToken() { 29 | localStorage.removeItem(tokenKey); 30 | localStorage.removeItem(refreshTokenKey); 31 | } -------------------------------------------------------------------------------- /Flip-front/src/views/about/About.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 37 | 38 | -------------------------------------------------------------------------------- /Flip-front/src/views/admin/Admin.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 77 | 78 | 93 | 94 | -------------------------------------------------------------------------------- /Flip-front/src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /Flip-front/src/views/tabs/admin/AdminAuthority.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /Flip-front/src/views/tabs/admin/AdminMail.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /Flip-front/src/views/tabs/main/AllPosts.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 58 | 59 | -------------------------------------------------------------------------------- /Flip-front/src/views/tabs/main/HotPosts.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 58 | 59 | -------------------------------------------------------------------------------- /Flip-front/src/views/user/RoleTest.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /Flip-front/vite.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import {defineConfig} from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import AutoImport from 'unplugin-auto-import/vite' 6 | import Components from 'unplugin-vue-components/vite' 7 | import {ElementPlusResolver} from 'unplugin-vue-components/resolvers' 8 | import {loadEnv} from "vite" 9 | 10 | // https://cn.vitejs.dev/config/ 11 | export default ({mode}) => defineConfig({ 12 | envDir: './env', 13 | plugins: [ 14 | vue(), 15 | AutoImport({ 16 | resolvers: [ElementPlusResolver()], 17 | }), 18 | Components({ 19 | resolvers: [ElementPlusResolver()], 20 | }), 21 | ], 22 | resolve: { 23 | alias: { 24 | '~': path.resolve(__dirname, '/'), //设置路径 25 | '@': path.resolve(__dirname, './src'), //设置别名 26 | }, 27 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'], /* https://cn.vitejs.dev/config/shared-options.html#resolve-extensions */ 28 | }, 29 | server: { 30 | port: 80, /* 指定前端端口 */ 31 | host: true, /* 指定服务器应该监听哪个 IP 地址。 如果将此设置为 0.0.0.0 或者 true 将监听所有地址,包括局域网和公网地址。 */ 32 | open: true, /* 启动Vite项目后在浏览器打开项目首页 */ 33 | proxy: { /* https://cn.vitejs.dev/config/server-options.html#server-proxy], https://zxuqian.cn/vite-proxy-config */ 34 | '/api' : { 35 | target: loadEnv(mode, './env').VITE_PROXY_TARGET, /*后端地址*/ 36 | changeOrigin: true, 37 | rewrite: (path) => path.replace(/^\/api/, ''), /* 重写,后端接收时不会有/api */ 38 | } 39 | }, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Flip - 前后端分离式论坛系统 2 | 3 | ![JDK](https://img.shields.io/badge/JDK-17-brightgreen) ![MySQL](https://img.shields.io/badge/MySQL-8-cb3838) ![SpringBoot](https://img.shields.io/badge/Spring%20Boot-3-blue) ![Vuw](https://img.shields.io/badge/Vue-3-orange) 4 | 5 | ### ✍️  项目描述 6 | 7 | Flip 是一款前后端分离的论坛系统,旨在为用户提供一个交流和分享信息的平台。 8 | 9 | > admin 的密码:1Qq.....(五个点) 10 | 11 | ### ✌️  项目涉及的技术内容 12 | 13 | - 前端技术栈:Vue.js,Element Plus,Vue Router,Axios,Pinia 等,构建工具为 Vite。 14 | - 后端技术栈:Spring Boot,Spring Security,MyBatis,MyBatis Plus,Spring Mail 等,构建工具为 Maven。 15 | - 中间件:Redis,ElasticSearch 等,后续会引入 RabbitMQ。 16 | - 数据库:MySQL 8.0。 17 | 18 | ### 😎  已实现功能 19 | 20 | - [x] 用户的登录与注册,使用 Spring Security 完成。 21 | - [x] 富文本编辑器,引入开源的 Vditor。 22 | - [x] 发布、编辑和浏览帖子。 23 | - [x] 标签和分类。 24 | - [x] 评论与回复。评论采用层级形式,回复采用楼中楼形式。 25 | - [x] 用户个人中心,支持头像修改和头像文件的裁剪。 26 | - [x] 账号设置。 27 | - [x] 后台管理。 28 | - [x] 搜索。 29 | - [x] 敏感词过滤。 30 | - [x] 移动端适配,响应式布局。 31 | 32 | ### ❎  待完成功能 33 | 34 | - [ ] 权限管理可视化操作。 35 | - [ ] 引入消息队列优化系统性能。 36 | - [ ] 更多待添加.... 37 | 38 | ### 🤞一些配置 39 | 40 | 1. 首次使用,请下载 releases 下的静态资源文件(主要是头像文件和 LOGO 文件),并配置 `application(-dev|-prod).yaml` 文件的 `upload.avatarPath` 和 `upload.staticPath` 路径。 41 | 2. 下载 ElasticSearch 后,需要安装 [analysis-ik](https://github.com/infinilabs/analysis-ik) 中文分词插件,具体请自行探索该插件和安装该插件的方法。 42 | 3. ElasticSearch 限制内存占用:将 `config` 目录下的 `jvm.options` 文件复制到 `config\jvm.options.d\` 目录下,并将该文件内容清空,粘贴如下内容到文件中: 43 | ```properties 44 | -Xms1g 45 | -Xmx2g 46 | ``` 47 | 4. 关闭 ElasticSearch 的安全功能(仅限本地测试):`elasticsearch.yml` 文件末尾添加 `xpack.security.enabled: false`。 48 | 49 | ### 🙈  系统截图 50 | 51 | #### (1) 论坛首页 52 | 53 | ![index](Flip-front/public/images/main.jpg) 54 | 55 | ---- 56 | 57 | ##### (2) 登录页 58 | 59 | ![](Flip-front/public/images/login.jpg) 60 | 61 | ---- 62 | 63 | ##### (3) 帖子详情页 64 | 65 | ![](Flip-front/public/images/post.jpg) 66 | 67 | ---- 68 | 69 | ##### (4) 楼中楼回复 70 | 71 | ![](Flip-front/public/images/replies.jpg) 72 | 73 | ---- 74 | 75 | ##### (5) 个人中心 76 | 77 | ![](Flip-front/public/images/profile.jpg) 78 | 79 | ---- 80 | 81 | ##### (6) 修改头像 82 | 83 | ![](Flip-front/public/images/avatar_change.jpg) 84 | 85 | ---- 86 | 87 | ##### (7) 搜索 88 | 89 | ![](Flip-front/public/images/search.jpg) 90 | 91 | ---- 92 | 93 | ##### (8) 标签页 94 | 95 | ![](Flip-front/public/images/tags.jpg) 96 | 97 | ---- 98 | 99 | ##### (9) 帖子发布页 100 | 101 | ![](Flip-front/public/images/write.jpg) 102 | 103 | ---- 104 | 105 | ##### (10) 抽屉编辑器 106 | 107 | ![](Flip-front/public/images/comment_drawer.jpg) 108 | 109 | ---- 110 | 111 | ##### (11) 后台管理 112 | 113 | ![](Flip-front/public/images/admin_panel.jpg) 114 | 115 | ---- 116 | 117 | ##### (12) 暗黑模式 118 | 119 | ![](Flip-front/public/images/replies_black.jpg) 120 | 121 | ---- 122 | 123 | ![](Flip-front/public/images/search_black.jpg) 124 | 125 | ---- 126 | 127 | ![](Flip-front/public/images/tags_black.jpg) 128 | 129 | ---- 130 | --------------------------------------------------------------------------------