完成backend的用户、分类、新闻相关接口的开发
This commit is contained in:
parent
73fee7d713
commit
1c187a00cf
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dir:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,10 +9,10 @@
|
|||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.aisi</groupId>
|
||||
<artifactId>chery</artifactId>
|
||||
<artifactId>NewClassifier</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<name>chery</name>
|
||||
<description>chery</description>
|
||||
<name>NewClassifier</name>
|
||||
<description>NewClassifier</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license>
|
||||
|
|
@ -95,6 +95,11 @@
|
|||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.aisi.newsclassifier.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* JPA 配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaConfig {
|
||||
}
|
||||
|
|
@ -27,8 +27,12 @@ public class SecurityConfig {
|
|||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/v3/api-docs/**","/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html").permitAll()
|
||||
.requestMatchers("/api/v1/user/register", "/api/v1/user/login").permitAll()
|
||||
// 分类和新闻接口通过 Controller 注解控制权限
|
||||
.requestMatchers("/api/v1/categories/**").permitAll()
|
||||
.requestMatchers("/api/v1/news/**").permitAll()
|
||||
.requestMatchers("/api/v1/user/info").authenticated()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.aisi.newsclassifier.controller;
|
||||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.dto.CategoryCreateDto;
|
||||
import com.aisi.newsclassifier.domain.vo.CategoryVo;
|
||||
import com.aisi.newsclassifier.service.CategoryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分类管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/categories")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "分类管理接口")
|
||||
public class CategoryController {
|
||||
|
||||
private final CategoryService categoryService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取所有分类")
|
||||
public RestBean<List<CategoryVo>> getAllCategories() {
|
||||
return categoryService.getAllCategories();
|
||||
}
|
||||
|
||||
@GetMapping("/with-count")
|
||||
@Operation(summary = "获取分类及新闻数量")
|
||||
public RestBean<List<CategoryVo>> getAllCategoriesWithNewsCount() {
|
||||
return categoryService.getAllCategoriesWithNewsCount();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取分类详情")
|
||||
public RestBean<CategoryVo> getCategoryDetail(
|
||||
@Parameter(description = "分类ID") @PathVariable Integer id) {
|
||||
return categoryService.getCategoryDetail(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建分类")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public RestBean<CategoryVo> createCategory(@Valid @RequestBody CategoryCreateDto createDto) {
|
||||
return categoryService.createCategory(createDto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新分类")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public RestBean<CategoryVo> updateCategory(
|
||||
@Parameter(description = "分类ID") @PathVariable Integer id,
|
||||
@Valid @RequestBody CategoryCreateDto updateDto) {
|
||||
return categoryService.updateCategory(id, updateDto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除分类")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public RestBean<Void> deleteCategory(
|
||||
@Parameter(description = "分类ID") @PathVariable Integer id) {
|
||||
return categoryService.deleteCategory(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.aisi.newsclassifier.controller;
|
||||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.RestCode;
|
||||
import com.aisi.newsclassifier.domain.dto.*;
|
||||
import com.aisi.newsclassifier.domain.vo.NewsVo;
|
||||
import com.aisi.newsclassifier.service.NewsService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 新闻管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/news")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "新闻管理接口")
|
||||
public class NewsController {
|
||||
|
||||
private final NewsService newsService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取新闻列表")
|
||||
public RestBean<PageResult<NewsVo>> getNewsList(NewsQueryDto queryDto) {
|
||||
return newsService.getNewsList(queryDto);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取新闻详情")
|
||||
public RestBean<NewsVo> getNewsDetail(
|
||||
@Parameter(description = "新闻ID") @PathVariable Long id) {
|
||||
return newsService.getNewsDetail(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建新闻")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public RestBean<NewsVo> createNews(@Valid @RequestBody NewsCreateDto createDto) {
|
||||
return newsService.createNews(createDto);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "更新新闻")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public RestBean<NewsVo> updateNews(@Valid @RequestBody NewsUpdateDto updateDto) {
|
||||
return newsService.updateNews(updateDto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除新闻")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public RestBean<Void> deleteNews(
|
||||
@Parameter(description = "新闻ID") @PathVariable Long id) {
|
||||
if (id == null)
|
||||
return RestBean.failure(RestCode.FAILURE);
|
||||
return newsService.deleteNews(id);
|
||||
}
|
||||
|
||||
@DeleteMapping("/batch")
|
||||
@Operation(summary = "批量删除新闻")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public RestBean<Void> batchDeleteNews(@RequestBody List<Long> ids) {
|
||||
return newsService.batchDeleteNews(ids);
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
@Operation(summary = "搜索新闻")
|
||||
public RestBean<PageResult<NewsVo>> searchNews(
|
||||
@Parameter(description = "搜索关键词") @RequestParam String keyword,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer size) {
|
||||
return newsService.searchNews(keyword, page, size);
|
||||
}
|
||||
|
||||
@GetMapping("/category/{categoryId}")
|
||||
@Operation(summary = "按分类获取新闻")
|
||||
public RestBean<PageResult<NewsVo>> getNewsByCategory(
|
||||
@Parameter(description = "分类ID") @PathVariable Integer categoryId,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer size) {
|
||||
return newsService.getNewsByCategory(categoryId, page, size);
|
||||
}
|
||||
|
||||
@GetMapping("/latest")
|
||||
@Operation(summary = "获取最新新闻")
|
||||
public RestBean<PageResult<NewsVo>> getLatestNews(
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer size) {
|
||||
return newsService.getLatestNews(page, size);
|
||||
}
|
||||
|
||||
@GetMapping("/statistics")
|
||||
@Operation(summary = "获取新闻统计数据")
|
||||
public RestBean<Map<String, Object>> getNewsStatistics() {
|
||||
return newsService.getNewsStatistics();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,7 @@ package com.aisi.newsclassifier.controller;
|
|||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.dto.UserDto;
|
||||
import com.aisi.newsclassifier.domain.entity.User;
|
||||
import com.aisi.newsclassifier.repository.UserRepository;
|
||||
import com.aisi.newsclassifier.domain.vo.UserVo;
|
||||
import com.aisi.newsclassifier.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
|
@ -20,7 +19,7 @@ public class UserController {
|
|||
|
||||
@GetMapping("info")
|
||||
@Operation(summary = "用户信息")
|
||||
public RestBean<String> getUserInfo() {
|
||||
public RestBean<UserVo> getUserInfo() {
|
||||
return userService.getUserInfo();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,17 @@ public class CustomUserDetails implements UserDetails {
|
|||
private final String username;
|
||||
private final String password;
|
||||
private final Collection<? extends GrantedAuthority> authorities;
|
||||
private final boolean enabled;
|
||||
@Getter
|
||||
private final String role;
|
||||
|
||||
public CustomUserDetails(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
|
||||
public CustomUserDetails(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities, boolean enabled, String role) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.authorities = authorities;
|
||||
this.enabled = enabled;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -53,6 +58,6 @@ public class CustomUserDetails implements UserDetails {
|
|||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.aisi.newsclassifier.domain.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分类创建/更新请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分类创建/更新请求")
|
||||
public class CategoryCreateDto {
|
||||
|
||||
@NotBlank(message = "分类名称不能为空")
|
||||
@Schema(description = "分类名称", required = true, example = "科技")
|
||||
private String name;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.aisi.newsclassifier.domain.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分类数据传输对象
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分类数据传输对象")
|
||||
public class CategoryDto {
|
||||
|
||||
@Schema(description = "分类ID")
|
||||
private Integer id;
|
||||
|
||||
@Schema(description = "分类名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "该分类下的新闻数量")
|
||||
private Integer newsCount;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.aisi.newsclassifier.domain.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 新闻创建请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "新闻创建请求")
|
||||
public class NewsCreateDto {
|
||||
|
||||
@NotBlank(message = "URL不能为空")
|
||||
@Schema(description = "新闻URL", required = true)
|
||||
private String url;
|
||||
|
||||
@NotBlank(message = "标题不能为空")
|
||||
@Schema(description = "标题", required = true)
|
||||
private String title;
|
||||
|
||||
@Schema(description = "分类ID")
|
||||
private Integer categoryId;
|
||||
|
||||
@Schema(description = "发布时间")
|
||||
private LocalDateTime publishTime;
|
||||
|
||||
@Schema(description = "作者")
|
||||
private String author;
|
||||
|
||||
@Schema(description = "来源(网易/36kr)")
|
||||
private String source;
|
||||
|
||||
@NotBlank(message = "内容不能为空")
|
||||
@Schema(description = "正文内容", required = true)
|
||||
private String content;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.aisi.newsclassifier.domain.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 新闻数据传输对象
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "新闻数据传输对象")
|
||||
public class NewsDto {
|
||||
|
||||
@Schema(description = "新闻ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "新闻URL")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "分类ID")
|
||||
private Integer categoryId;
|
||||
|
||||
@Schema(description = "分类名称")
|
||||
private String categoryName;
|
||||
|
||||
@Schema(description = "发布时间")
|
||||
private LocalDateTime publishTime;
|
||||
|
||||
@Schema(description = "作者")
|
||||
private String author;
|
||||
|
||||
@Schema(description = "来源(网易/36kr)")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "正文内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "入库时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.aisi.newsclassifier.domain.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 新闻查询参数DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "新闻查询参数")
|
||||
public class NewsQueryDto {
|
||||
|
||||
@Schema(description = "页码", example = "1")
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页大小", example = "20")
|
||||
private Integer size = 20;
|
||||
|
||||
@Schema(description = "分类ID")
|
||||
private Integer categoryId;
|
||||
|
||||
@Schema(description = "来源网站(网易/36kr)")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "搜索关键词(标题或内容)")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "排序字段", example = "createdAt")
|
||||
private String sortBy = "createdAt";
|
||||
|
||||
@Schema(description = "排序方向", example = "DESC")
|
||||
private String sortOrder = "DESC";
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.aisi.newsclassifier.domain.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 新闻更新请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "新闻更新请求")
|
||||
public class NewsUpdateDto {
|
||||
|
||||
@Schema(description = "新闻ID", required = true)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "分类ID")
|
||||
private Integer categoryId;
|
||||
|
||||
@Schema(description = "发布时间")
|
||||
private LocalDateTime publishTime;
|
||||
|
||||
@Schema(description = "作者")
|
||||
private String author;
|
||||
|
||||
@Schema(description = "来源")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "正文内容")
|
||||
private String content;
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.aisi.newsclassifier.domain.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页结果封装类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "分页结果")
|
||||
public class PageResult<T> {
|
||||
|
||||
@Schema(description = "数据列表")
|
||||
private List<T> records;
|
||||
|
||||
@Schema(description = "总记录数")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "当前页")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页大小")
|
||||
private Integer size;
|
||||
|
||||
@Schema(description = "总页数")
|
||||
private Integer pages;
|
||||
|
||||
/**
|
||||
* 创建分页结果
|
||||
*/
|
||||
public static <T> PageResult<T> of(List<T> records, Long total, Integer page, Integer size) {
|
||||
return new PageResult<>(
|
||||
records,
|
||||
total,
|
||||
page,
|
||||
size,
|
||||
(int) Math.ceil((double) total / size)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空的分页结果
|
||||
*/
|
||||
public static <T> PageResult<T> empty(Integer page, Integer size) {
|
||||
return new PageResult<>(
|
||||
List.of(),
|
||||
0L,
|
||||
page,
|
||||
size,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,33 @@ package com.aisi.newsclassifier.domain.dto;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户数据传输对象")
|
||||
public class UserDto {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
@Schema(description = "邮件")
|
||||
|
||||
@Schema(description = "邮箱")
|
||||
private String email;
|
||||
@Schema(description = "密码")
|
||||
private String password; // 注册时需要
|
||||
|
||||
@Schema(description = "密码(注册/登录时需要)")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "状态(1=正常 0=禁用)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "角色(USER=普通用户,ADMIN=管理员)")
|
||||
private String role;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.aisi.newsclassifier.domain.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 新闻分类实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "news_category", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_name", columnNames = {"name"})
|
||||
})
|
||||
@Data
|
||||
public class Category {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "name", nullable = false, unique = true, length = 50)
|
||||
private String name;
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.aisi.newsclassifier.domain.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 新闻实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "news", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_url", columnNames = {"url"}),
|
||||
@UniqueConstraint(name = "uk_content_hash", columnNames = {"content_hash"})
|
||||
}, indexes = {
|
||||
@Index(name = "idx_category_id", columnList = "category_id"),
|
||||
@Index(name = "idx_source", columnList = "source")
|
||||
})
|
||||
@Data
|
||||
public class News {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "url", nullable = false, unique = true, length = 500)
|
||||
private String url;
|
||||
|
||||
@Column(name = "title", nullable = false, length = 255)
|
||||
private String title;
|
||||
|
||||
@Column(name = "category_id")
|
||||
private Integer categoryId;
|
||||
|
||||
@Column(name = "publish_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime publishTime;
|
||||
|
||||
@Column(name = "author", length = 100)
|
||||
private String author;
|
||||
|
||||
@Column(name = "source", length = 50)
|
||||
private String source;
|
||||
|
||||
@Column(name = "content", nullable = false, columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
@JsonIgnore
|
||||
@Column(name = "content_hash", nullable = false, unique = true, length = 64)
|
||||
private String contentHash;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 生成内容SHA256哈希值(用于去重)
|
||||
*/
|
||||
public static String generateContentHash(String content) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(content.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256算法不可用", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内容并自动生成哈希值
|
||||
*/
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
this.contentHash = generateContentHash(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内容并指定哈希值(用于爬虫已计算哈希的情况)
|
||||
*/
|
||||
public void setContentWithHash(String content, String contentHash) {
|
||||
this.content = content;
|
||||
this.contentHash = contentHash;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
if (content != null && contentHash == null) {
|
||||
contentHash = generateContentHash(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,59 @@
|
|||
package com.aisi.newsclassifier.domain.entity;
|
||||
|
||||
import com.aisi.newsclassifier.domain.enums.Role;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@Data
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO) // 自增组件
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false,unique = true) // 非空+唯一
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String username;
|
||||
|
||||
@ToString.Exclude
|
||||
@JsonIgnore
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(unique = true)
|
||||
private String email;
|
||||
|
||||
@Column(name = "status", nullable = false, columnDefinition = "TINYINT DEFAULT 1 COMMENT '1=正常 0=禁用'")
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "role", nullable = false, length = 20, columnDefinition = "VARCHAR(20) NOT NULL DEFAULT 'USER' COMMENT '用户角色:USER=普通用户,ADMIN=管理员'")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Role role = Role.USER;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 检查用户是否启用
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return status != null && status == 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package com.aisi.newsclassifier.domain.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 用户角色枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum Role {
|
||||
|
||||
/**
|
||||
* 普通用户
|
||||
*/
|
||||
USER("ROLE_USER", "普通用户"),
|
||||
|
||||
/**
|
||||
* 管理员
|
||||
*/
|
||||
ADMIN("ROLE_ADMIN", "管理员");
|
||||
|
||||
private final String authority;
|
||||
private final String description;
|
||||
|
||||
Role(String authority, String description) {
|
||||
this.authority = authority;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库字符串转换为枚举
|
||||
*/
|
||||
public static Role fromString(String role) {
|
||||
if (role == null) {
|
||||
return USER;
|
||||
}
|
||||
try {
|
||||
return Role.valueOf(role.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return USER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数据库存储格式
|
||||
*/
|
||||
public String toDbValue() {
|
||||
return this.name();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.aisi.newsclassifier.domain.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "分类视图对象")
|
||||
public class CategoryVo {
|
||||
|
||||
@Schema(description = "分类ID")
|
||||
private Integer id;
|
||||
|
||||
@Schema(description = "分类名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "该分类下的新闻数量")
|
||||
private Integer newsCount;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.aisi.newsclassifier.domain.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "新闻视图对象")
|
||||
public class NewsVo {
|
||||
|
||||
@Schema(description = "新闻ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "新闻URL")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "分类ID")
|
||||
private Integer categoryId;
|
||||
|
||||
@Schema(description = "分类名称")
|
||||
private String categoryName;
|
||||
|
||||
@Schema(description = "发布时间")
|
||||
private LocalDateTime publishTime;
|
||||
|
||||
@Schema(description = "作者")
|
||||
private String author;
|
||||
|
||||
@Schema(description = "来源(网易/36kr)")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "正文内容(摘要或完整内容)")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "入库时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.aisi.newsclassifier.domain.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户视图对象")
|
||||
public class UserVo {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "状态(1=正常 0=禁用)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "角色(USER=普通用户,ADMIN=管理员)")
|
||||
private String role;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.aisi.newsclassifier.handler;
|
||||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.RestCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理权限拒绝异常
|
||||
*/
|
||||
@ExceptionHandler(AuthorizationDeniedException.class)
|
||||
public RestBean<Void> handleAuthorizationDenied(AuthorizationDeniedException e) {
|
||||
log.warn("权限拒绝: {}", e.getMessage());
|
||||
return RestBean.failure(RestCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理其他异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public RestBean<Void> handleException(Exception e) {
|
||||
log.error("系统异常: ", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.aisi.newsclassifier.repository;
|
||||
|
||||
import com.aisi.newsclassifier.domain.entity.Category;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 分类数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface CategoryRepository extends JpaRepository<Category, Integer> {
|
||||
|
||||
/**
|
||||
* 根据名称查找分类
|
||||
*/
|
||||
Optional<Category> findByName(String name);
|
||||
|
||||
/**
|
||||
* 检查名称是否存在
|
||||
*/
|
||||
boolean existsByName(String name);
|
||||
|
||||
/**
|
||||
* 查询所有分类,按ID排序
|
||||
*/
|
||||
List<Category> findAllByOrderByIdAsc();
|
||||
|
||||
/**
|
||||
* 根据名称模糊查询
|
||||
*/
|
||||
List<Category> findByNameContainingIgnoreCase(String name);
|
||||
|
||||
/**
|
||||
* 统计每个分类下的新闻数量
|
||||
*/
|
||||
@Query("SELECT new map(c.id as id, c.name as name, COUNT(n.id) as newsCount) " +
|
||||
"FROM Category c LEFT JOIN News n ON c.id = n.categoryId " +
|
||||
"GROUP BY c.id, c.name")
|
||||
List<Object[]> findAllWithNewsCount();
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.aisi.newsclassifier.repository;
|
||||
|
||||
import com.aisi.newsclassifier.domain.entity.News;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 新闻数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface NewsRepository extends JpaRepository<News, Long>, JpaSpecificationExecutor<News> {
|
||||
|
||||
/**
|
||||
* 根据URL查找新闻(用于去重检查)
|
||||
*/
|
||||
News findByUrl(String url);
|
||||
|
||||
/**
|
||||
* 根据内容哈希查找新闻(用于去重检查)
|
||||
*/
|
||||
News findByContentHash(String contentHash);
|
||||
|
||||
/**
|
||||
* 检查URL是否存在
|
||||
*/
|
||||
boolean existsByUrl(String url);
|
||||
|
||||
/**
|
||||
* 检查内容哈希是否存在
|
||||
*/
|
||||
boolean existsByContentHash(String contentHash);
|
||||
|
||||
/**
|
||||
* 根据分类ID分页查询新闻
|
||||
*/
|
||||
Page<News> findByCategoryIdOrderByCreatedAtDesc(Integer categoryId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据来源分页查询新闻
|
||||
*/
|
||||
Page<News> findBySourceOrderByCreatedAtDesc(String source, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 全文搜索新闻(标题或内容包含关键词)
|
||||
*/
|
||||
@Query("SELECT n FROM News n WHERE " +
|
||||
"(n.title LIKE %:keyword% OR n.content LIKE %:keyword%) " +
|
||||
"ORDER BY n.createdAt DESC")
|
||||
Page<News> searchByKeyword(@Param("keyword") String keyword, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计指定分类下的新闻数量
|
||||
*/
|
||||
Long countByCategoryId(Integer categoryId);
|
||||
|
||||
/**
|
||||
* 统计指定来源的新闻数量
|
||||
*/
|
||||
Long countBySource(String source);
|
||||
|
||||
/**
|
||||
* 查询最新新闻
|
||||
*/
|
||||
@Query("SELECT n FROM News n ORDER BY n.createdAt DESC")
|
||||
Page<News> findLatestNews(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计每个分类的新闻数量
|
||||
*/
|
||||
@Query("SELECT n.categoryId, COUNT(n) FROM News n GROUP BY n.categoryId")
|
||||
List<Object[]> countByCategory();
|
||||
|
||||
/**
|
||||
* 统计每个来源的新闻数量
|
||||
*/
|
||||
@Query("SELECT n.source, COUNT(n) FROM News n GROUP BY n.source")
|
||||
List<Object[]> countBySourceGroup();
|
||||
|
||||
/**
|
||||
* 批量删除新闻
|
||||
*/
|
||||
void deleteAllByIdIn(List<Long> ids);
|
||||
}
|
||||
|
|
@ -3,15 +3,35 @@ package com.aisi.newsclassifier.repository;
|
|||
import com.aisi.newsclassifier.domain.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User,Long> {
|
||||
import java.util.Optional;
|
||||
|
||||
User findByUsername(String username);
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
User findByEmail(String email);
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
User findByUsernameAndPassword(String username, String password);
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
User findByEmailAndPassword(String email, String password);
|
||||
Optional<User> findByUsernameAndPassword(String username, String password);
|
||||
|
||||
User findByEmailContaining(String keyword);
|
||||
Optional<User> findByEmailAndPassword(String email, String password);
|
||||
|
||||
/**
|
||||
* 根据用户名查找启用的用户
|
||||
*/
|
||||
Optional<User> findByUsernameAndStatus(String username, Integer status);
|
||||
|
||||
/**
|
||||
* 根据邮箱查找启用的用户
|
||||
*/
|
||||
Optional<User> findByEmailAndStatus(String email, Integer status);
|
||||
|
||||
/**
|
||||
* 检查用户名是否存在
|
||||
*/
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.aisi.newsclassifier.service;
|
||||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.dto.CategoryCreateDto;
|
||||
import com.aisi.newsclassifier.domain.vo.CategoryVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分类服务接口
|
||||
*/
|
||||
public interface CategoryService {
|
||||
|
||||
/**
|
||||
* 获取所有分类
|
||||
*/
|
||||
RestBean<List<CategoryVo>> getAllCategories();
|
||||
|
||||
/**
|
||||
* 获取分类详情
|
||||
*/
|
||||
RestBean<CategoryVo> getCategoryDetail(Integer id);
|
||||
|
||||
/**
|
||||
* 创建分类
|
||||
*/
|
||||
RestBean<CategoryVo> createCategory(CategoryCreateDto createDto);
|
||||
|
||||
/**
|
||||
* 更新分类
|
||||
*/
|
||||
RestBean<CategoryVo> updateCategory(Integer id, CategoryCreateDto updateDto);
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
*/
|
||||
RestBean<Void> deleteCategory(Integer id);
|
||||
|
||||
/**
|
||||
* 获取分类及其新闻数量
|
||||
*/
|
||||
RestBean<List<CategoryVo>> getAllCategoriesWithNewsCount();
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.aisi.newsclassifier.service;
|
||||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.dto.*;
|
||||
import com.aisi.newsclassifier.domain.vo.NewsVo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 新闻服务接口
|
||||
*/
|
||||
public interface NewsService {
|
||||
|
||||
/**
|
||||
* 分页查询新闻列表
|
||||
*/
|
||||
RestBean<PageResult<NewsVo>> getNewsList(NewsQueryDto queryDto);
|
||||
|
||||
/**
|
||||
* 获取新闻详情
|
||||
*/
|
||||
RestBean<NewsVo> getNewsDetail(Long id);
|
||||
|
||||
/**
|
||||
* 创建新闻(支持去重检查)
|
||||
*/
|
||||
RestBean<NewsVo> createNews(NewsCreateDto createDto);
|
||||
|
||||
/**
|
||||
* 更新新闻
|
||||
*/
|
||||
RestBean<NewsVo> updateNews(NewsUpdateDto updateDto);
|
||||
|
||||
/**
|
||||
* 删除新闻
|
||||
*/
|
||||
RestBean<Void> deleteNews(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除新闻
|
||||
*/
|
||||
RestBean<Void> batchDeleteNews(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 搜索新闻
|
||||
*/
|
||||
RestBean<PageResult<NewsVo>> searchNews(String keyword, Integer page, Integer size);
|
||||
|
||||
/**
|
||||
* 按分类查询新闻
|
||||
*/
|
||||
RestBean<PageResult<NewsVo>> getNewsByCategory(Integer categoryId, Integer page, Integer size);
|
||||
|
||||
/**
|
||||
* 获取最新新闻
|
||||
*/
|
||||
RestBean<PageResult<NewsVo>> getLatestNews(Integer page, Integer size);
|
||||
|
||||
/**
|
||||
* 获取新闻统计数据
|
||||
*/
|
||||
RestBean<Map<String, Object>> getNewsStatistics();
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package com.aisi.newsclassifier.service;
|
|||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.dto.UserDto;
|
||||
import com.aisi.newsclassifier.domain.vo.UserVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
public interface UserService {
|
||||
|
||||
RestBean<String> getUserInfo();
|
||||
RestBean<UserVo> getUserInfo();
|
||||
|
||||
RestBean<String> register(UserDto userDto);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
package com.aisi.newsclassifier.service.impl;
|
||||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.RestCode;
|
||||
import com.aisi.newsclassifier.domain.dto.CategoryCreateDto;
|
||||
import com.aisi.newsclassifier.domain.entity.Category;
|
||||
import com.aisi.newsclassifier.domain.vo.CategoryVo;
|
||||
import com.aisi.newsclassifier.repository.CategoryRepository;
|
||||
import com.aisi.newsclassifier.repository.NewsRepository;
|
||||
import com.aisi.newsclassifier.service.CategoryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 分类服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryServiceImpl implements CategoryService {
|
||||
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final NewsRepository newsRepository;
|
||||
|
||||
@Override
|
||||
public RestBean<List<CategoryVo>> getAllCategories() {
|
||||
try {
|
||||
List<Category> categories = categoryRepository.findAllByOrderByIdAsc();
|
||||
List<CategoryVo> categoryVos = categories.stream()
|
||||
.map(this::convertToVo)
|
||||
.collect(Collectors.toList());
|
||||
return RestBean.success(categoryVos);
|
||||
} catch (Exception e) {
|
||||
log.error("获取分类列表失败", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<CategoryVo> getCategoryDetail(Integer id) {
|
||||
try {
|
||||
return categoryRepository.findById(id)
|
||||
.map(category -> RestBean.success(convertToVo(category)))
|
||||
.orElseGet(() -> RestBean.failure(RestCode.DATA_NOT_FOUND, null));
|
||||
} catch (Exception e) {
|
||||
log.error("获取分类详情失败, id={}", id, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestBean<CategoryVo> createCategory(CategoryCreateDto createDto) {
|
||||
try {
|
||||
// 检查名称是否已存在
|
||||
if (categoryRepository.existsByName(createDto.getName())) {
|
||||
return RestBean.failure(400, "分类名称已存在", null);
|
||||
}
|
||||
|
||||
Category category = new Category();
|
||||
category.setName(createDto.getName());
|
||||
|
||||
Category savedCategory = categoryRepository.save(category);
|
||||
return RestBean.success(convertToVo(savedCategory));
|
||||
} catch (Exception e) {
|
||||
log.error("创建分类失败", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestBean<CategoryVo> updateCategory(Integer id, CategoryCreateDto updateDto) {
|
||||
try {
|
||||
Category category = categoryRepository.findById(id)
|
||||
.orElse(null);
|
||||
|
||||
if (category == null) {
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND, null);
|
||||
}
|
||||
|
||||
// 检查名称是否被其他分类使用
|
||||
if (!category.getName().equals(updateDto.getName()) &&
|
||||
categoryRepository.existsByName(updateDto.getName())) {
|
||||
return RestBean.failure(400, "分类名称已存在", null);
|
||||
}
|
||||
|
||||
category.setName(updateDto.getName());
|
||||
|
||||
Category updatedCategory = categoryRepository.save(category);
|
||||
return RestBean.success(convertToVo(updatedCategory));
|
||||
} catch (Exception e) {
|
||||
log.error("更新分类失败, id={}", id, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestBean<Void> deleteCategory(Integer id) {
|
||||
try {
|
||||
if (!categoryRepository.existsById(id)) {
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND, null);
|
||||
}
|
||||
|
||||
// TODO: 检查是否有新闻使用该分类,有则不允许删除或需要先转移分类
|
||||
|
||||
categoryRepository.deleteById(id);
|
||||
return RestBean.success(RestCode.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
log.error("删除分类失败, id={}", id, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<List<CategoryVo>> getAllCategoriesWithNewsCount() {
|
||||
try {
|
||||
List<Category> categories = categoryRepository.findAllByOrderByIdAsc();
|
||||
List<CategoryVo> categoryVos = categories.stream()
|
||||
.map(category -> {
|
||||
CategoryVo vo = convertToVo(category);
|
||||
// 统计该分类下的新闻数量
|
||||
Long count = newsRepository.countByCategoryId(category.getId());
|
||||
vo.setNewsCount(count.intValue());
|
||||
return vo;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return RestBean.success(categoryVos);
|
||||
} catch (Exception e) {
|
||||
log.error("获取分类及新闻数量失败", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
private CategoryVo convertToVo(Category category) {
|
||||
CategoryVo vo = new CategoryVo();
|
||||
vo.setId(category.getId());
|
||||
vo.setName(category.getName());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,15 +18,21 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
User user = userRepository.findByUsername(username);
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException("User not found");
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
|
||||
|
||||
// 检查用户状态
|
||||
if (!user.isEnabled()) {
|
||||
throw new UsernameNotFoundException("用户已被禁用: " + username);
|
||||
}
|
||||
|
||||
return new CustomUserDetails(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
"*********",
|
||||
List.of(() -> "USER")
|
||||
user.getPassword(),
|
||||
List.of(() -> user.getRole().getAuthority()),
|
||||
user.isEnabled(),
|
||||
user.getRole().name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,360 @@
|
|||
package com.aisi.newsclassifier.service.impl;
|
||||
|
||||
import com.aisi.newsclassifier.domain.RestBean;
|
||||
import com.aisi.newsclassifier.domain.RestCode;
|
||||
import com.aisi.newsclassifier.domain.dto.*;
|
||||
import com.aisi.newsclassifier.domain.entity.Category;
|
||||
import com.aisi.newsclassifier.domain.entity.News;
|
||||
import com.aisi.newsclassifier.domain.vo.NewsVo;
|
||||
import com.aisi.newsclassifier.repository.CategoryRepository;
|
||||
import com.aisi.newsclassifier.repository.NewsRepository;
|
||||
import com.aisi.newsclassifier.service.NewsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 新闻服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class NewsServiceImpl implements NewsService {
|
||||
|
||||
private final NewsRepository newsRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
|
||||
@Override
|
||||
public RestBean<PageResult<NewsVo>> getNewsList(NewsQueryDto queryDto) {
|
||||
try {
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(
|
||||
queryDto.getSortOrder().equalsIgnoreCase("ASC")
|
||||
? Sort.Order.asc(queryDto.getSortBy())
|
||||
: Sort.Order.desc(queryDto.getSortBy())
|
||||
);
|
||||
Pageable pageable = PageRequest.of(queryDto.getPage() - 1, queryDto.getSize(), sort);
|
||||
|
||||
// 构建查询条件
|
||||
Specification<News> spec = buildSpecification(queryDto);
|
||||
|
||||
// 执行查询
|
||||
Page<News> newsPage = newsRepository.findAll(spec, pageable);
|
||||
|
||||
// 转换为VO
|
||||
List<NewsVo> newsVos = convertToVoList(newsPage.getContent());
|
||||
|
||||
// 构建分页结果
|
||||
PageResult<NewsVo> pageResult = PageResult.of(
|
||||
newsVos,
|
||||
newsPage.getTotalElements(),
|
||||
queryDto.getPage(),
|
||||
queryDto.getSize()
|
||||
);
|
||||
|
||||
return RestBean.success(pageResult);
|
||||
} catch (Exception e) {
|
||||
log.error("查询新闻列表失败", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<NewsVo> getNewsDetail(Long id) {
|
||||
try {
|
||||
News news = newsRepository.findById(id).orElse(null);
|
||||
if (news == null) {
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND, null);
|
||||
}
|
||||
return RestBean.success(convertToVo(news));
|
||||
} catch (Exception e) {
|
||||
log.error("获取新闻详情失败, id={}", id, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestBean<NewsVo> createNews(NewsCreateDto createDto) {
|
||||
try {
|
||||
// 检查URL是否重复
|
||||
if (newsRepository.existsByUrl(createDto.getUrl())) {
|
||||
return RestBean.failure(400, "该URL的新闻已存在", null);
|
||||
}
|
||||
|
||||
// 检查分类是否存在
|
||||
if (createDto.getCategoryId() != null) {
|
||||
if (!categoryRepository.existsById(createDto.getCategoryId())) {
|
||||
return RestBean.failure(400, "指定的分类不存在", null);
|
||||
}
|
||||
}
|
||||
|
||||
News news = new News();
|
||||
news.setUrl(createDto.getUrl());
|
||||
news.setTitle(createDto.getTitle());
|
||||
news.setCategoryId(createDto.getCategoryId());
|
||||
news.setPublishTime(createDto.getPublishTime());
|
||||
news.setAuthor(createDto.getAuthor());
|
||||
news.setSource(createDto.getSource());
|
||||
news.setContent(createDto.getContent()); // 会自动生成contentHash
|
||||
|
||||
News savedNews = newsRepository.save(news);
|
||||
return RestBean.success(convertToVo(savedNews));
|
||||
} catch (Exception e) {
|
||||
log.error("创建新闻失败", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestBean<NewsVo> updateNews(NewsUpdateDto updateDto) {
|
||||
try {
|
||||
News news = newsRepository.findById(updateDto.getId()).orElse(null);
|
||||
if (news == null) {
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND, null);
|
||||
}
|
||||
|
||||
// 检查分类是否存在
|
||||
if (updateDto.getCategoryId() != null) {
|
||||
if (!categoryRepository.existsById(updateDto.getCategoryId())) {
|
||||
return RestBean.failure(400, "指定的分类不存在", null);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (StringUtils.hasText(updateDto.getTitle())) {
|
||||
news.setTitle(updateDto.getTitle());
|
||||
}
|
||||
if (updateDto.getCategoryId() != null) {
|
||||
news.setCategoryId(updateDto.getCategoryId());
|
||||
}
|
||||
if (updateDto.getPublishTime() != null) {
|
||||
news.setPublishTime(updateDto.getPublishTime());
|
||||
}
|
||||
if (StringUtils.hasText(updateDto.getAuthor())) {
|
||||
news.setAuthor(updateDto.getAuthor());
|
||||
}
|
||||
if (StringUtils.hasText(updateDto.getSource())) {
|
||||
news.setSource(updateDto.getSource());
|
||||
}
|
||||
if (StringUtils.hasText(updateDto.getContent())) {
|
||||
news.setContent(updateDto.getContent()); // 会自动重新生成contentHash
|
||||
}
|
||||
|
||||
News updatedNews = newsRepository.save(news);
|
||||
return RestBean.success(convertToVo(updatedNews));
|
||||
} catch (Exception e) {
|
||||
log.error("更新新闻失败, id={}", updateDto.getId(), e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestBean<Void> deleteNews(Long id) {
|
||||
try {
|
||||
if (!newsRepository.existsById(id)) {
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND, null);
|
||||
}
|
||||
newsRepository.deleteById(id);
|
||||
return RestBean.success(RestCode.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
log.error("删除新闻失败, id={}", id, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RestBean<Void> batchDeleteNews(List<Long> ids) {
|
||||
try {
|
||||
newsRepository.deleteAllByIdIn(ids);
|
||||
return RestBean.success(RestCode.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
log.error("批量删除新闻失败, ids={}", ids, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<PageResult<NewsVo>> searchNews(String keyword, Integer page, Integer size) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by("createdAt").descending());
|
||||
Page<News> newsPage = newsRepository.searchByKeyword(keyword, pageable);
|
||||
|
||||
List<NewsVo> newsVos = convertToVoList(newsPage.getContent());
|
||||
PageResult<NewsVo> pageResult = PageResult.of(
|
||||
newsVos,
|
||||
newsPage.getTotalElements(),
|
||||
page,
|
||||
size
|
||||
);
|
||||
|
||||
return RestBean.success(pageResult);
|
||||
} catch (Exception e) {
|
||||
log.error("搜索新闻失败, keyword={}", keyword, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<PageResult<NewsVo>> getNewsByCategory(Integer categoryId, Integer page, Integer size) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by("createdAt").descending());
|
||||
Page<News> newsPage = newsRepository.findByCategoryIdOrderByCreatedAtDesc(categoryId, pageable);
|
||||
|
||||
List<NewsVo> newsVos = convertToVoList(newsPage.getContent());
|
||||
PageResult<NewsVo> pageResult = PageResult.of(
|
||||
newsVos,
|
||||
newsPage.getTotalElements(),
|
||||
page,
|
||||
size
|
||||
);
|
||||
|
||||
return RestBean.success(pageResult);
|
||||
} catch (Exception e) {
|
||||
log.error("按分类查询新闻失败, categoryId={}", categoryId, e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<PageResult<NewsVo>> getLatestNews(Integer page, Integer size) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size);
|
||||
Page<News> newsPage = newsRepository.findLatestNews(pageable);
|
||||
|
||||
List<NewsVo> newsVos = convertToVoList(newsPage.getContent());
|
||||
PageResult<NewsVo> pageResult = PageResult.of(
|
||||
newsVos,
|
||||
newsPage.getTotalElements(),
|
||||
page,
|
||||
size
|
||||
);
|
||||
|
||||
return RestBean.success(pageResult);
|
||||
} catch (Exception e) {
|
||||
log.error("获取最新新闻失败", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<Map<String, Object>> getNewsStatistics() {
|
||||
try {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 总新闻数
|
||||
stats.put("totalNews", newsRepository.count());
|
||||
|
||||
// 按分类统计
|
||||
List<Object[]> categoryStats = newsRepository.countByCategory();
|
||||
Map<String, Long> categoryMap = new HashMap<>();
|
||||
for (Object[] stat : categoryStats) {
|
||||
Integer categoryId = (Integer) stat[0];
|
||||
Long count = (Long) stat[1];
|
||||
categoryRepository.findById(categoryId).ifPresent(category -> {
|
||||
categoryMap.put(category.getName(), count);
|
||||
});
|
||||
}
|
||||
stats.put("categoryStats", categoryMap);
|
||||
|
||||
// 按来源统计
|
||||
List<Object[]> sourceStats = newsRepository.countBySourceGroup();
|
||||
Map<String, Long> sourceMap = new HashMap<>();
|
||||
for (Object[] stat : sourceStats) {
|
||||
String source = (String) stat[0];
|
||||
Long count = (Long) stat[1];
|
||||
sourceMap.put(source, count);
|
||||
}
|
||||
stats.put("sourceStats", sourceMap);
|
||||
|
||||
return RestBean.success(stats);
|
||||
} catch (Exception e) {
|
||||
log.error("获取新闻统计失败", e);
|
||||
return RestBean.failure(RestCode.SYSTEM_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*/
|
||||
|
||||
private Specification<News> buildSpecification(NewsQueryDto queryDto) {
|
||||
return (root, query, cb) -> {
|
||||
// 不带前缀,直接用 Predicate
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
// 分类筛选
|
||||
if (queryDto.getCategoryId() != null) {
|
||||
predicates.add(cb.equal(root.get("categoryId"), queryDto.getCategoryId()));
|
||||
}
|
||||
|
||||
// 来源筛选
|
||||
if (StringUtils.hasText(queryDto.getSource())) {
|
||||
predicates.add(cb.equal(root.get("source"), queryDto.getSource()));
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(queryDto.getKeyword())) {
|
||||
String keyword = "%" + queryDto.getKeyword() + "%";
|
||||
predicates.add(cb.or(
|
||||
cb.like(root.get("title"), keyword),
|
||||
cb.like(root.get("content"), keyword)
|
||||
));
|
||||
}
|
||||
|
||||
// 返回组合条件
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 转换单个新闻为VO
|
||||
*/
|
||||
private NewsVo convertToVo(News news) {
|
||||
NewsVo vo = new NewsVo();
|
||||
vo.setId(news.getId());
|
||||
vo.setUrl(news.getUrl());
|
||||
vo.setTitle(news.getTitle());
|
||||
vo.setCategoryId(news.getCategoryId());
|
||||
vo.setPublishTime(news.getPublishTime());
|
||||
vo.setAuthor(news.getAuthor());
|
||||
vo.setSource(news.getSource());
|
||||
vo.setContent(news.getContent());
|
||||
vo.setCreatedAt(news.getCreatedAt());
|
||||
|
||||
// 查询分类信息
|
||||
if (news.getCategoryId() != null) {
|
||||
categoryRepository.findById(news.getCategoryId()).ifPresent(category -> {
|
||||
vo.setCategoryName(category.getName());
|
||||
});
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换新闻列表为VO列表
|
||||
*/
|
||||
private List<NewsVo> convertToVoList(List<News> newsList) {
|
||||
return newsList.stream()
|
||||
.map(this::convertToVo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ import com.aisi.newsclassifier.domain.RestBean;
|
|||
import com.aisi.newsclassifier.domain.RestCode;
|
||||
import com.aisi.newsclassifier.domain.dto.UserDto;
|
||||
import com.aisi.newsclassifier.domain.entity.User;
|
||||
import com.aisi.newsclassifier.domain.enums.Role;
|
||||
import com.aisi.newsclassifier.domain.vo.UserVo;
|
||||
import com.aisi.newsclassifier.repository.UserRepository;
|
||||
import com.aisi.newsclassifier.service.UserService;
|
||||
import com.aisi.newsclassifier.utils.JwtUtil;
|
||||
import com.aisi.newsclassifier.utils.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
|
@ -21,43 +21,75 @@ public class UserServiceImpl implements UserService {
|
|||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final UserRepository userRepository;
|
||||
private SecurityUtils securityUtils;
|
||||
private final SecurityUtils securityUtils;
|
||||
|
||||
@Override
|
||||
public RestBean<String> getUserInfo() {
|
||||
public RestBean<UserVo> getUserInfo() {
|
||||
String username = SecurityUtils.getUsername();
|
||||
User user = userRepository.findByUsername(username);
|
||||
return RestBean.success(user.toString());
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElse(null);
|
||||
if (user == null) {
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND);
|
||||
}
|
||||
// 转换为 UserVo 返回
|
||||
UserVo userVo = new UserVo();
|
||||
userVo.setId(user.getId());
|
||||
userVo.setUsername(user.getUsername());
|
||||
userVo.setEmail(user.getEmail());
|
||||
userVo.setStatus(user.getStatus());
|
||||
userVo.setRole(user.getRole().name());
|
||||
userVo.setCreatedAt(user.getCreatedAt());
|
||||
userVo.setUpdatedAt(user.getUpdatedAt());
|
||||
return RestBean.success(userVo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<String> register(UserDto userDto) {
|
||||
if (userRepository.findByUsername(userDto.getUsername()) != null) {
|
||||
return RestBean.failure(RestCode.FAILURE,"Username is already in use");
|
||||
// 检查用户名是否存在
|
||||
if (userRepository.existsByUsername(userDto.getUsername())) {
|
||||
return RestBean.failure(400, "用户名已被使用", null);
|
||||
}
|
||||
if (userRepository.findByEmail(userDto.getEmail()) != null) {
|
||||
return RestBean.failure(RestCode.FAILURE, "Email is already in use");
|
||||
// 检查邮箱是否存在
|
||||
if (userRepository.existsByEmail(userDto.getEmail())) {
|
||||
return RestBean.failure(400, "邮箱已被使用", null);
|
||||
}
|
||||
|
||||
User user = new User();
|
||||
user.setUsername(userDto.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
|
||||
user.setEmail(userDto.getEmail());
|
||||
String token = jwtUtil.generateToken(user.getId(),user.getUsername());
|
||||
user.setRole(Role.USER); // 新注册用户默认为普通用户
|
||||
// 默认状态为1(正常),已在实体类中设置
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
// 生成token
|
||||
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
return RestBean.success(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestBean<String> login(UserDto userDto) {
|
||||
User user = userRepository.findByUsername(userDto.getUsername());
|
||||
// 查找用户
|
||||
User user = userRepository.findByUsername(userDto.getUsername())
|
||||
.orElse(null);
|
||||
|
||||
if (user == null) {
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND, "User does not exist");
|
||||
return RestBean.failure(RestCode.DATA_NOT_FOUND, "用户不存在");
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!user.isEnabled()) {
|
||||
return RestBean.failure(403, "用户已被禁用", null);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!passwordEncoder.matches(userDto.getPassword(), user.getPassword())) {
|
||||
return RestBean.failure(RestCode.USERNAME_OR_PASSWORD_ERROR,"Username or password is incorrect");
|
||||
return RestBean.failure(RestCode.USERNAME_OR_PASSWORD_ERROR, "用户名或密码错误");
|
||||
}
|
||||
// 刷新token
|
||||
|
||||
// 生成token
|
||||
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
return RestBean.success(RestCode.SUCCESS, token);
|
||||
return RestBean.success(token);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,28 @@ package com.aisi.newsclassifier.utils;
|
|||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
|
||||
private final long expiration = 1000 * 60 * 60; // 1小时
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
private final long expiration = 1000 * 60 * 60 *24; // 24小时
|
||||
|
||||
private Key getSigningKey() {
|
||||
// 确保密钥足够长(至少 256 位 = 32 字节)
|
||||
if (secret.length() < 32) {
|
||||
throw new IllegalArgumentException("JWT secret must be at least 32 characters long");
|
||||
}
|
||||
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public String generateToken(Long userId,String username) {
|
||||
return Jwts.builder()
|
||||
|
|
@ -19,7 +31,7 @@ public class JwtUtil {
|
|||
.claim("id", userId)
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date(System.currentTimeMillis() + expiration))
|
||||
.signWith(key)
|
||||
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
|
||||
.compact();
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +50,7 @@ public class JwtUtil {
|
|||
|
||||
private Jws<Claims> parseClaims(String token) {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(key)
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:news}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:news}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
|
||||
username: ${DB_USER:root}
|
||||
password: ${DB_PASS:root}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:news}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:news}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
|
||||
username: ${DB_USER:root}
|
||||
password: ${DB_PASS:root}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
|
|
|||
|
|
@ -6,3 +6,6 @@ spring:
|
|||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
jwt:
|
||||
secret: newsClassifierSecretKeyForJWT2024MustBeLongEnoughForHS256Algorithm
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
-- ============================================
|
||||
-- 新闻分类器数据库初始化脚本
|
||||
-- ============================================
|
||||
|
||||
-- 1. 创建分类表
|
||||
CREATE TABLE IF NOT EXISTS news_category (
|
||||
id INT NOT NULL AUTO_INCREMENT COMMENT '分类ID',
|
||||
name VARCHAR(50) NOT NULL COMMENT '分类名称',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='新闻分类表';
|
||||
|
||||
-- 2. 插入初始分类数据
|
||||
INSERT INTO news_category (id, name) VALUES
|
||||
(1, '娱乐'),
|
||||
(2, '体育'),
|
||||
(3, '财经'),
|
||||
(4, '科技'),
|
||||
(5, '军事'),
|
||||
(6, '汽车'),
|
||||
(7, '政务'),
|
||||
(8, '健康'),
|
||||
(9, 'AI')
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name);
|
||||
|
||||
-- 3. 创建新闻表
|
||||
CREATE TABLE IF NOT EXISTS news (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||
url VARCHAR(500) NOT NULL COMMENT '新闻原始URL',
|
||||
title VARCHAR(255) NOT NULL COMMENT '新闻标题',
|
||||
category_id INT NULL COMMENT '新闻分类ID',
|
||||
publish_time DATETIME NULL COMMENT '发布时间',
|
||||
author VARCHAR(100) NULL COMMENT '作者/来源',
|
||||
source VARCHAR(50) NULL COMMENT '新闻来源(网易/36kr)',
|
||||
content LONGTEXT NOT NULL COMMENT '新闻正文',
|
||||
content_hash CHAR(64) NOT NULL COMMENT '正文内容hash,用于去重',
|
||||
created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_url (url),
|
||||
UNIQUE KEY uk_content_hash (content_hash),
|
||||
KEY idx_category_id (category_id),
|
||||
KEY idx_source (source),
|
||||
KEY idx_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='新闻表';
|
||||
|
||||
-- 4. 添加外键约束(可选,根据需求决定是否启用)
|
||||
-- ALTER TABLE news
|
||||
-- ADD CONSTRAINT fk_news_category
|
||||
-- FOREIGN KEY (category_id)
|
||||
-- REFERENCES news_category(id)
|
||||
-- ON DELETE SET NULL
|
||||
-- ON UPDATE CASCADE;
|
||||
|
||||
|
||||
CREATE TABLE users (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(255) DEFAULT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1=正常 0=禁用',
|
||||
`role` VARCHAR(20) NOT NULL DEFAULT 'USER' COMMENT '用户角色:USER=普通用户,ADMIN=管理员',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY uk_users_username (`username`),
|
||||
UNIQUE KEY uk_users_email (`email`),
|
||||
KEY idx_users_role (`role`)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
Loading…
Reference in New Issue