其实是毕设答辩前的垂死挣扎
知识库系统代码详解
后端
http文件夹(测试接口)
ebook.http
### 列表接口
GET http://localhost:8880/ebook/list?page=1&size=1001
### 电子书保存接口
POST http://localhost:8880/ebook/save
Content-Type: application/json
{}
###
POST http://localhost:80/api/item
Content-Type: application/x-www-form-urlencoded
id=99&content=new-element
###
aspect - LogAspect
配置AOP,打印接口耗时、请求参数、返回参数
同一个IP,一天只能对一个文档点赞一次
增加日志流水号,方便生产运维
package com.li.wiki.aspect;
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/** 定义一个切点 */
@Pointcut("execution(public * com.li.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
@Resource
private SnowFlake snowFlake;
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
RequestContext.setRemoteAddr(getRemoteIp(request));
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
/**
* 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
* @param request
* @return
*/
public String getRemoteIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
config
CorsConfig配置类(前后端交互)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true)
.maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
}
}
JacksonConfig
解决前后端交互Long类型精度丢失的问题
/**
* 统一注解,解决前后端交互Long类型精度丢失的问题
*/
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
return objectMapper;
}
}
SpringMvcConfig
拦截器还需要增加个配置类
前后端增加登录拦截
首页显示电子书信息:文档数、阅读数、点赞数
前端增加数值统计组件,显示统计数值
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
LoginInterceptor loginInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/test/**",
"/redis/**",
"/user/login",
"/category/all",
"/ebook/list",
"/doc/all/**",
"/doc/vote/**",
"/doc/find-content/**",
"/ebook-snapshot/**",
"/ebook/upload/avatar",
"/file/**"
);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/file/**").addResourceLocations("file:D:/file/wiki/");
}
}
WebSocketConfig
- 新建一个Java配置类,注入ServerEndpointExporter 配置,如果是使用springboot内置的tomcat此配置必须,如果是使用的是外部tomcat容器此步骤请忽略。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WikiApplication启动类
@ComponentScan("com.li")
@SpringBootApplication
@MapperScan("com.li.wiki.mapper")
@EnableScheduling
@EnableAsync
public class WikiApplication {
private static final Logger LOG = LoggerFactory.getLogger(WikiApplication.class);
public static void main(String[] args) {
SpringApplication app = new SpringApplication(WikiApplication.class);
Environment env = app.run(args).getEnvironment();
LOG.info("启动成功!!");
LOG.info("地址: \thttp://127.0.0.1:{}", env.getProperty("server.port"));
}
}
Controller控制器类
CategoryController
- 分类Controller
@RestController
@RequestMapping("/category")
public class CategoryController {
@Resource
private CategoryService categoryService;
@GetMapping("/all")
public CommonResp all() {
CommonResp<List<CategoryQueryResp>> resp = new CommonResp<>();
List<CategoryQueryResp> list = categoryService.all();
resp.setContent(list);
return resp;
}
@GetMapping("/list")
public CommonResp list(@Valid CategoryQueryReq req) {
CommonResp<PageResp<CategoryQueryResp>> resp = new CommonResp<>();
PageResp<CategoryQueryResp> list = categoryService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody CategorySaveReq req) {
CommonResp resp = new CommonResp<>();
categoryService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
categoryService.delete(id);
return resp;
}
}
ControllerExceptionHandler
统一异常处理、数据预处理
/**
* 统一异常处理、数据预处理等
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public CommonResp validExceptionHandler(BindException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
commonResp.setSuccess(false);
commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return commonResp;
}
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public CommonResp validExceptionHandler(BusinessException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("业务异常:{}", e.getCode().getDesc());
commonResp.setSuccess(false);
commonResp.setMessage(e.getCode().getDesc());
return commonResp;
}
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public CommonResp validExceptionHandler(Exception e) {
CommonResp commonResp = new CommonResp();
LOG.error("系统异常:", e);
commonResp.setSuccess(false);
commonResp.setMessage("系统出现异常,请联系管理员");
return commonResp;
}
}
DocController
- 文档Controller
@RestController
@RequestMapping("/doc")
public class DocController {
@Resource
private DocService docService;
@GetMapping("/all/{ebookId}")
public CommonResp all(@PathVariable Long ebookId) {
CommonResp<List<DocQueryResp>> resp = new CommonResp<>();
List<DocQueryResp> list = docService.all(ebookId);
resp.setContent(list);
return resp;
}
@GetMapping("/list")
public CommonResp list(@Valid DocQueryReq req) {
CommonResp<PageResp<DocQueryResp>> resp = new CommonResp<>();
PageResp<DocQueryResp> list = docService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody DocSaveReq req) {
CommonResp resp = new CommonResp<>();
docService.save(req);
return resp;
}
@DeleteMapping("/delete/{idsStr}")
public CommonResp delete(@PathVariable String idsStr) {
CommonResp resp = new CommonResp<>();
List<String> list = Arrays.asList(idsStr.split(","));
docService.delete(list);
return resp;
}
@GetMapping("/find-content/{id}")
public CommonResp findContent(@PathVariable Long id) {
CommonResp<String> resp = new CommonResp<>();
String content = docService.findContent(id);
resp.setContent(content);
return resp;
}
@GetMapping("/vote/{id}")
public CommonResp vote(@PathVariable Long id) {
CommonResp commonResp = new CommonResp();
docService.vote(id);
return commonResp;
}
}
EbookController
- 电子书Controller
@RestController
@RequestMapping("/ebook")
public class EbookController {
private static final Logger LOG = LoggerFactory.getLogger(EbookController.class);
@Resource
private EbookService ebookService;
@GetMapping("/list")
public CommonResp list(@Valid EbookQueryReq req) {
CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
PageResp<EbookQueryResp> list = ebookService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody EbookSaveReq req) {
CommonResp resp = new CommonResp<>();
ebookService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
ebookService.delete(id);
return resp;
}
@RequestMapping("/upload/avatar")
public CommonResp upload(@RequestParam MultipartFile avatar) throws IOException {
LOG.info("上传文件开始:{}", avatar);
LOG.info("文件名:{}", avatar.getOriginalFilename());
LOG.info("文件大小:{}", avatar.getSize());
// 保存文件到本地
String fileName = avatar.getOriginalFilename();
String fullPath = "D:/file/wiki/" + fileName;
File dest = new File(fullPath);
avatar.transferTo(dest);
LOG.info(dest.getAbsolutePath());
return new CommonResp();
}
}
EbookSnapshotController
- 快照
@RestController
@RequestMapping("/ebook-snapshot")
public class EbookSnapshotController {
@Resource
private EbookSnapshotService ebookSnapshotService;
@GetMapping("/get-statistic")
public CommonResp getStatistic() {
List<StatisticResp> statisticResp = ebookSnapshotService.getStatistic();
CommonResp<List<StatisticResp>> commonResp = new CommonResp<>();
commonResp.setContent(statisticResp);
return commonResp;
}
@GetMapping("/get-30-statistic")
public CommonResp get30Statistic() {
List<StatisticResp> statisticResp = ebookSnapshotService.get30Statistic();
CommonResp<List<StatisticResp>> commonResp = new CommonResp<>();
commonResp.setContent(statisticResp);
return commonResp;
}
}
UserController
- 用户Controller
- 用到了SnowFlake,雪花自增ID
- 用到了RedisTemplate
@RestController
@RequestMapping("/user")
public class UserController {
private static final Logger LOG = LoggerFactory.getLogger(UserController.class);
@Resource
private UserService userService;
@Resource
private SnowFlake snowFlake;
@Resource
private RedisTemplate redisTemplate;
@GetMapping("/list")
public CommonResp list(@Valid UserQueryReq req) {
CommonResp<PageResp<UserQueryResp>> resp = new CommonResp<>();
PageResp<UserQueryResp> list = userService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody UserSaveReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp resp = new CommonResp<>();
userService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
userService.delete(id);
return resp;
}
@PostMapping("/reset-password")
public CommonResp resetPassword(@Valid @RequestBody UserResetPasswordReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp resp = new CommonResp<>();
userService.resetPassword(req);
return resp;
}
@PostMapping("/login")
public CommonResp login(@Valid @RequestBody UserLoginReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp<UserLoginResp> resp = new CommonResp<>();
UserLoginResp userLoginResp = userService.login(req);
Long token = snowFlake.nextId();
LOG.info("生成单点登录token:{},并放入redis中", token);
userLoginResp.setToken(token.toString());
redisTemplate.opsForValue().set(token.toString(), JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS);
resp.setContent(userLoginResp);
return resp;
}
@GetMapping("/logout/{token}")
public CommonResp logout(@PathVariable String token) {
CommonResp resp = new CommonResp<>();
redisTemplate.delete(token);
LOG.info("从redis中删除token: {}", token);
return resp;
}
}
domain实体类
数据库中对应的实体类,略
exception异常类
BusinessException
- 增加校验用户名不能重复,增加自定义异常
package com.li.wiki.exception;
public class BusinessException extends RuntimeException{
private BusinessExceptionCode code;
public BusinessException (BusinessExceptionCode code) {
super(code.getDesc());
this.code = code;
}
public BusinessExceptionCode getCode() {
return code;
}
public void setCode(BusinessExceptionCode code) {
this.code = code;
}
/**
* 不写入堆栈信息,提高性能
*/
@Override
public Throwable fillInStackTrace() {
return this;
}
}
BusinessExceptionCode
- USER_LOGIN_NAME_EXIST("登录名已存在")
- LOGIN_USER_ERROR("用户名不存在或密码错误")
- VOTE_REPEAT("您已点赞过")
public enum BusinessExceptionCode {
USER_LOGIN_NAME_EXIST("登录名已存在"),
LOGIN_USER_ERROR("用户名不存在或密码错误"),
VOTE_REPEAT("您已点赞过"),
;
private String desc;
BusinessExceptionCode(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
interceptor拦截器类
LoginInterceptor
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);
@Resource
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LoginInterceptor 开始 -------------");
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
// OPTIONS请求不做校验,
// 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
if(request.getMethod().toUpperCase().equals("OPTIONS")){
return true;
}
String path = request.getRequestURL().toString();
LOG.info("接口登录拦截:,path:{}", path);
//获取header的token参数
String token = request.getHeader("token");
LOG.info("登录校验开始,token:{}", token);
if (token == null || token.isEmpty()) {
LOG.info( "token为空,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
Object object = redisTemplate.opsForValue().get(token);
if (object == null) {
LOG.warn( "token无效,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
LOG.info("已登录:{}", object);
LoginUserContext.setUser(JSON.parseObject((String) object, UserLoginResp.class));
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// LOG.info("LogInterceptor 结束");
}
}
Job类文件夹
DocJob
- 更新电子书信息,增加日志流水号
@Component
public class DocJob {
private static final Logger LOG = LoggerFactory.getLogger(DocJob.class);
@Resource
private DocService docService;
@Resource
private SnowFlake snowFlake;
/**
* 每30秒更新电子书信息
*/
@Scheduled(cron = "5/30 * * * * ?")
public void cron() {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
LOG.info("更新电子书下的文档数据开始");
long start = System.currentTimeMillis();
docService.updateEbookInfo();
LOG.info("更新电子书下的文档数据结束,耗时:{}毫秒", System.currentTimeMillis() - start);
}
}
EbookSnapshotJob
@Component
public class EbookSnapshotJob {
private static final Logger LOG = LoggerFactory.getLogger(EbookSnapshotJob.class);
@Resource
private EbookSnapshotService ebookSnapshotService;
@Resource
private SnowFlake snowFlake;
/**
* 自定义cron表达式跑批
* 只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过
*/
@Scheduled(cron = "0 0/1 * * * ?")
public void doSnapshot() {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
LOG.info("生成今日电子书快照开始");
Long start = System.currentTimeMillis();
ebookSnapshotService.genSnapshot();
LOG.info("生成今日电子书快照结束,耗时:{}毫秒", System.currentTimeMillis() - start);
}
}
mapper类
自动生成的,不用管,mapper映射文件
req类 - resp类
封装请求参数和返回参数,和domain类似,多了一些校验
Service类
CategoryService
- 使用PageHelper分页
- 使用snowFlak保存,自增ID
@Service
public class CategoryService {
private static final Logger LOG = LoggerFactory.getLogger(CategoryService.class);
@Resource
private CategoryMapper categoryMapper;
@Resource
private SnowFlake snowFlake;
public List<CategoryQueryResp> all() {
CategoryExample categoryExample = new CategoryExample();
categoryExample.setOrderByClause("sort asc");
List<Category> categoryList = categoryMapper.selectByExample(categoryExample);
// 列表复制
List<CategoryQueryResp> list = CopyUtil.copyList(categoryList, CategoryQueryResp.class);
return list;
}
public PageResp<CategoryQueryResp> list(CategoryQueryReq req) {
CategoryExample categoryExample = new CategoryExample();
categoryExample.setOrderByClause("sort asc");
CategoryExample.Criteria criteria = categoryExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Category> categoryList = categoryMapper.selectByExample(categoryExample);
PageInfo<Category> pageInfo = new PageInfo<>(categoryList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<CategoryQueryResp> list = CopyUtil.copyList(categoryList, CategoryQueryResp.class);
PageResp<CategoryQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(CategorySaveReq req) {
Category category = CopyUtil.copy(req, Category.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
category.setId(snowFlake.nextId());
categoryMapper.insert(category);
} else {
// 更新
categoryMapper.updateByPrimaryKey(category);
}
}
public void delete(Long id) {
categoryMapper.deleteByPrimaryKey(id);
}
}
DocService
- DocMapperCust,自定义SQL,观看数+1,点赞数+1,更新电子书信息。
@Service
public class DocService {
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
@Resource
private DocMapper docMapper;
@Resource
private DocMapperCust docMapperCust;
@Resource
private ContentMapper contentMapper;
@Resource
private SnowFlake snowFlake;
@Resource
public RedisUtil redisUtil;
@Resource
public WsService wsService;
// @Resource
// private RocketMQTemplate rocketMQTemplate;
public List<DocQueryResp> all(Long ebookId) {
DocExample docExample = new DocExample();
docExample.createCriteria().andEbookIdEqualTo(ebookId);
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
public PageResp<DocQueryResp> list(DocQueryReq req) {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
DocExample.Criteria criteria = docExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Doc> docList = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(docList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
PageResp<DocQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
@Transactional
public void save(DocSaveReq req) {
Doc doc = CopyUtil.copy(req, Doc.class);
Content content = CopyUtil.copy(req, Content.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
doc.setId(snowFlake.nextId());
doc.setViewCount(0);
doc.setVoteCount(0);
docMapper.insert(doc);
content.setId(doc.getId());
contentMapper.insert(content);
} else {
// 更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
if (count == 0) {
contentMapper.insert(content);
}
}
}
public void delete(Long id) {
docMapper.deleteByPrimaryKey(id);
}
public void delete(List<String> ids) {
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.createCriteria();
criteria.andIdIn(ids);
docMapper.deleteByExample(docExample);
}
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
// 文档阅读数+1
docMapperCust.increaseViewCount(id);
if (ObjectUtils.isEmpty(content)) {
return "";
} else {
return content.getContent();
}
}
/**
* 点赞
*/
public void vote(Long id) {
// docMapperCust.increaseVoteCount(id);
// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();
if (redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 5000)) {
docMapperCust.increaseVoteCount(id);
} else {
throw new BusinessException(BusinessExceptionCode.VOTE_REPEAT);
}
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
String logId = MDC.get("LOG_ID");
wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
// rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");
}
public void updateEbookInfo() {
docMapperCust.updateEbookInfo();
}
}
EbookService
@Service
public class EbookService {
private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);
@Resource
private EbookMapper ebookMapper;
@Resource
private SnowFlake snowFlake;
public PageResp<EbookQueryResp> list(EbookQueryReq req) {
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getName())) {
criteria.andNameLike("%" + req.getName() + "%");
}
if (!ObjectUtils.isEmpty(req.getCategoryId2())) {
criteria.andCategory2IdEqualTo(req.getCategoryId2());
}
PageHelper.startPage(req.getPage(), req.getSize());
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<EbookQueryResp> list = CopyUtil.copyList(ebookList, EbookQueryResp.class);
PageResp<EbookQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(EbookSaveReq req) {
Ebook ebook = CopyUtil.copy(req, Ebook.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
ebook.setId(snowFlake.nextId());
ebook.setDocCount(0);
ebook.setViewCount(0);
ebook.setVoteCount(0);
ebookMapper.insert(ebook);
} else {
// 更新
ebookMapper.updateByPrimaryKey(ebook);
}
}
public void delete(Long id) {
ebookMapper.deleteByPrimaryKey(id);
}
}
EbookSnapshotService
- 获取首页数值数据:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长
- 30天数值统计
@Service
public class EbookSnapshotService {
@Resource
private EbookSnapshotMapperCust ebookSnapshotMapperCust;
public void genSnapshot() {
ebookSnapshotMapperCust.genSnapshot();
}
/**
* 获取首页数值数据:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长
*/
public List<StatisticResp> getStatistic() {
return ebookSnapshotMapperCust.getStatistic();
}
/**
* 30天数值统计
*/
public List<StatisticResp> get30Statistic() {
return ebookSnapshotMapperCust.get30Statistic();
}
}
UserService
@Service
public class UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
@Resource
private UserMapper userMapper;
@Resource
private SnowFlake snowFlake;
public PageResp<UserQueryResp> list(UserQueryReq req) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getLoginName())) {
criteria.andLoginNameEqualTo(req.getLoginName());
}
PageHelper.startPage(req.getPage(), req.getSize());
List<User> userList = userMapper.selectByExample(userExample);
PageInfo<User> pageInfo = new PageInfo<>(userList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<UserQueryResp> list = CopyUtil.copyList(userList, UserQueryResp.class);
PageResp<UserQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(UserSaveReq req) {
User user = CopyUtil.copy(req, User.class);
if (ObjectUtils.isEmpty(req.getId())) {
User userDB = selectByLoginName(req.getLoginName());
if (ObjectUtils.isEmpty(userDB)) {
// 新增
user.setId(snowFlake.nextId());
userMapper.insert(user);
} else {
// 用户名已存在
throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
}
} else {
// 更新
user.setLoginName(null);
user.setPassword(null);
userMapper.updateByPrimaryKeySelective(user);
}
}
public void delete(Long id) {
userMapper.deleteByPrimaryKey(id);
}
public User selectByLoginName(String LoginName) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
criteria.andLoginNameEqualTo(LoginName);
List<User> userList = userMapper.selectByExample(userExample);
if (CollectionUtils.isEmpty(userList)) {
return null;
} else {
return userList.get(0);
}
}
/**
* 修改密码
*/
public void resetPassword(UserResetPasswordReq req) {
User user = CopyUtil.copy(req, User.class);
userMapper.updateByPrimaryKeySelective(user);
}
/**
* 登录
*/
public UserLoginResp login(UserLoginReq req) {
User userDb = selectByLoginName(req.getLoginName());
if (ObjectUtils.isEmpty(userDb)) {
// 用户名不存在
LOG.info("用户名不存在, {}", req.getLoginName());
throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
} else {
if (userDb.getPassword().equals(req.getPassword())) {
// 登录成功
UserLoginResp userLoginResp = CopyUtil.copy(userDb, UserLoginResp.class);
return userLoginResp;
} else {
// 密码不对
LOG.info("密码不对, 输入密码:{}, 数据库密码:{}", req.getPassword(), userDb.getPassword());
throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
}
}
}
}
WsService - WebSocketServer
@Service
public class WsService {
@Resource
public WebSocketServer webSocketServer;
@Async
public void sendInfo(String message, String logId) {
MDC.put("LOG_ID", logId);
webSocketServer.sendInfo(message);
}
}
@Component
@ServerEndpoint("/ws/{token}")
public class WebSocketServer {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 每个客户端一个token
*/
private String token = "";
private static HashMap<String, Session> map = new HashMap<>();
/**
* 连接成功
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
map.put(token, session);
this.token = token;
LOG.info("有新连接:token:{},session id:{},当前连接数:{}", token, session.getId(), map.size());
}
/**
* 连接关闭
*/
@OnClose
public void onClose(Session session) {
map.remove(this.token);
LOG.info("连接关闭,token:{},session id:{}!当前连接数:{}", this.token, session.getId(), map.size());
}
/**
* 收到消息
*/
@OnMessage
public void onMessage(String message, Session session) {
LOG.info("收到消息:{},内容:{}", token, message);
}
/**
* 连接错误
*/
@OnError
public void onError(Session session, Throwable error) {
LOG.error("发生错误", error);
}
/**
* 群发消息
*/
public void sendInfo(String message) {
for (String token : map.keySet()) {
Session session = map.get(token);
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
LOG.error("推送消息失败:{},内容:{}", token, message);
}
LOG.info("推送消息:{},内容:{}", token, message);
}
}
}
utils类
CopyUtil
- 用于单体复制和列表复制
public class CopyUtil {
/**
* 单体复制
*/
public static <T> T copy(Object source, Class<T> clazz) {
if (source == null) {
return null;
}
T obj = null;
try {
obj = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
BeanUtils.copyProperties(source, obj);
return obj;
}
/**
* 列表复制
*/
public static <T> List<T> copyList(List source, Class<T> clazz) {
List<T> target = new ArrayList<>();
if (!CollectionUtils.isEmpty(source)){
for (Object c: source) {
T obj = copy(c, clazz);
target.add(obj);
}
}
return target;
}
}
RedisUtil
@Component
public class RedisUtil {
private static final Logger LOG = LoggerFactory.getLogger(RedisUtil.class);
@Resource
private RedisTemplate redisTemplate;
/**
* true:不存在,放一个KEY
* false:已存在
* @param key
* @param second
* @return
*/
public boolean validateRepeat(String key, long second) {
if (redisTemplate.hasKey(key)) {
LOG.info("key已存在:{}", key);
return false;
} else {
LOG.info("key不存在,放入:{},过期 {} 秒", key, second);
redisTemplate.opsForValue().set(key, key, second, TimeUnit.SECONDS);
return true;
}
}
}
SnowFlake
- Twitter的分布式自增ID雪花算法
/**
* Twitter的分布式自增ID雪花算法
**/
@Component
public class SnowFlake
resource - mapper
- 自动生成的mapper类,SQL
application.properties
server.port=8880
test.hello=Hello4
# 增加数据库连接
spring.datasource.url=jdbc:mysql://localhost:3307/wiki?serverTimezone=UTC&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 配置mybatis所有Mapper.xml所在的路径
mybatis.mapper-locations=classpath:/mapper/**/*.xml
# 打印所有的sql日志:sql, 参数, 结果
logging.level.com.li.wiki.mapper=trace
# redis配置
spring.redis.host=localhost
spring.redis.port=6379
前端
public - js - index.html
- 可用于配置开始页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="<%= BASE_URL %>js/md5.js"></script>
<script src="<%= BASE_URL %>js/session-storage.js"></script>
<!--<script src="<%= BASE_URL %>js/echarts_5.0.2.min.js"></script>-->
<script src="https://lib.baomitu.com/echarts/5.0.2/echarts.min.js"></script>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div style="width: 400px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
margin: -50px 0 0 -200px;font-family: YouYuan;
color: rgba(0, 0, 0, 1) !important;
font-size: 20px !important;
font-weight: 400;">
欢迎来到知识库!
</div>
</div>
<!-- built files will be auto injected -->
</body>
</html>
the-footer.vue
- 配置底部信息,以及配置WebSocket
<template>
<a-layout-footer style="text-align: center">
知识库<span v-show="user.id">,欢迎:{{user.name}}</span>
</a-layout-footer>
</template>
<script lang="ts">
import { defineComponent, computed, onMounted } from 'vue';
import store from "@/store";
import {Tool} from "@/util/tool";
import { notification } from 'ant-design-vue';
export default defineComponent({
name: 'the-footer',
setup() {
const user = computed(() => store.state.user);
let websocket: any;
let token: any;
const onOpen = () => {
console.log('WebSocket连接成功,状态码:', websocket.readyState)
};
const onMessage = (event: any) => {
console.log('WebSocket收到消息:', event.data);
notification['info']({
message: '收到消息',
description: event.data,
});
};
const onError = () => {
console.log('WebSocket连接错误,状态码:', websocket.readyState)
};
const onClose = () => {
console.log('WebSocket连接关闭,状态码:', websocket.readyState)
};
const initWebSocket = () => {
// 连接成功
websocket.onopen = onOpen;
// 收到消息的回调
websocket.onmessage = onMessage;
// 连接错误
websocket.onerror = onError;
// 连接关闭的回调
websocket.onclose = onClose;
};
onMounted(() => {
// WebSocket
if ('WebSocket' in window) {
token = Tool.uuid(10);
// 连接地址:ws://127.0.0.1:8880/ws/xxx
websocket = new WebSocket(process.env.VUE_APP_WS_SERVER + '/ws/' + token);
initWebSocket()
// 关闭
// websocket.close();
} else {
alert('当前浏览器 不支持')
}
});
return {
user
}
}
});
</script>
the-header.vue
- 用于配置头部信息,axios获取后端接口
<template>
<a-layout-header class="header">
<div class="logo">知识库</div>
<a-menu
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="/">
<router-link to="/">首页</router-link>
</a-menu-item>
<a-menu-item key="/admin/user" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/user">用户管理</router-link>
</a-menu-item>
<a-menu-item key="/admin/ebook" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
<a-menu-item key="/admin/category" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a-popconfirm
title="确认退出登录?"
ok-text="是"
cancel-text="否"
@confirm="logout()"
>
<a class="login-menu" v-show="user.id">
<span>退出登录</span>
</a>
</a-popconfirm>
<a class="login-menu" v-show="user.id">
<span>您好:{{user.name}}</span>
</a>
<a class="login-menu" v-show="!user.id" @click="showLoginModal">
<span>登录</span>
</a>
</a-menu>
<a-modal
title="登录"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登录名">
<a-input v-model:value="loginUser.loginName" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password" />
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import store from "@/store";
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'the-header',
setup () {
// 登录后保存
const user = computed(() => store.state.user);
// 用来登录
const loginUser = ref({
loginName: "test",
password: "test"
});
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 登录
const login = () => {
console.log("开始登录");
loginModalLoading.value = true;
loginUser.value.password = hexMd5(loginUser.value.password + KEY);
axios.post('/user/login', loginUser.value).then((response) => {
loginModalLoading.value = false;
const data = response.data;
if (data.success) {
loginModalVisible.value = false;
message.success("登录成功!");
store.commit("setUser", data.content);
} else {
message.error(data.message);
}
});
};
// 退出登录
const logout = () => {
console.log("退出登录开始");
axios.get('/user/logout/' + user.value.token).then((response) => {
const data = response.data;
if (data.success) {
message.success("退出登录成功!");
store.commit("setUser", {});
} else {
message.error(data.message);
}
});
};
return {
loginModalVisible,
loginModalLoading,
showLoginModal,
loginUser,
login,
user,
logout
}
}
});
</script>
<style>
.logo {
width: 120px;
height: 31px;
/*background: rgba(255, 255, 255, 0.2);*/
/*margin: 16px 28px 16px 0;*/
float: left;
color: white;
font-size: 18px;
}
.login-menu {
float: right;
color: white;
padding-left: 10px;
}
</style>
the-welcome.vue
- 欢迎页
<template>
<div>
<a-row>
<a-col :span="24">
<a-card>
<a-row>
<a-col :span="8">
<a-statistic title="总阅读量" :value="statistic.viewCount">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="总点赞量" :value="statistic.voteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="点赞率" :value="statistic.voteCount / statistic.viewCount * 100"
:precision="2"
suffix="%"
:value-style="{ color: '#cf1322' }">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<br>
<a-row :gutter="16">
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic title="今日阅读" :value="statistic.todayViewCount" style="margin-right: 50px">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="今日点赞" :value="statistic.todayVoteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic
title="预计今日阅读"
:value="statistic.todayViewIncrease"
:value-style="{ color: '#0000ff' }"
>
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic
title="预计今日阅读增长"
:value="statistic.todayViewIncreaseRateAbs"
:precision="2"
suffix="%"
class="demo-class"
:value-style="statistic.todayViewIncreaseRate < 0 ? { color: '#3f8600' } : { color: '#cf1322' }"
>
<template #prefix>
<arrow-down-outlined v-if="statistic.todayViewIncreaseRate < 0"/>
<arrow-up-outlined v-if="statistic.todayViewIncreaseRate >= 0"/>
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<br>
<a-row>
<a-col :span="24" id="main-col">
<div id="main" style="width: 100%;height:300px;"></div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import axios from 'axios';
declare let echarts: any;
export default defineComponent({
name: 'the-welcome',
setup () {
const statistic = ref();
statistic.value = {};
const getStatistic = () => {
axios.get('/ebook-snapshot/get-statistic').then((response) => {
const data = response.data;
if (data.success) {
const statisticResp = data.content;
statistic.value.viewCount = statisticResp[1].viewCount;
statistic.value.voteCount = statisticResp[1].voteCount;
statistic.value.todayViewCount = statisticResp[1].viewIncrease;
statistic.value.todayVoteCount = statisticResp[1].voteIncrease;
// 按分钟计算当前时间点,占一天的百分比
const now = new Date();
const nowRate = (now.getHours() * 60 + now.getMinutes()) / (60 * 24);
// console.log(nowRate)
statistic.value.todayViewIncrease = parseInt(String(statisticResp[1].viewIncrease / nowRate));
// todayViewIncreaseRate:今日预计增长率
statistic.value.todayViewIncreaseRate = (statistic.value.todayViewIncrease - statisticResp[0].viewIncrease) / statisticResp[0].viewIncrease * 100;
statistic.value.todayViewIncreaseRateAbs = Math.abs(statistic.value.todayViewIncreaseRate);
}
});
};
const init30DayEcharts = (list: any) => {
// 发布生产后出现问题:切到别的页面,再切回首页,报表显示不出来
// 解决方法:把原来的id=main的区域清空,重新初始化
const mainDom = document.getElementById('main-col');
if (mainDom) {
mainDom.innerHTML = '<div id="main" style="width: 100%;height:300px;"></div>';
}
// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(document.getElementById('main'));
const xAxis = [];
const seriesView = [];
const seriesVote = [];
for (let i = 0; i < list.length; i++) {
const record = list[i];
xAxis.push(record.date);
seriesView.push(record.viewIncrease);
seriesVote.push(record.voteIncrease);
}
// 指定图表的配置项和数据
const option = {
title: {
text: '30天趋势图'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['总阅读量', '总点赞量']
},
grid: {
left: '1%',
right: '3%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxis
},
yAxis: {
type: 'value'
},
series: [
{
name: '总阅读量',
type: 'line',
// stack: '总量', 不堆叠
data: seriesView,
smooth: true
},
{
name: '总点赞量',
type: 'line',
// stack: '总量', 不堆叠
data: seriesVote,
smooth: true
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
};
const get30DayStatistic = () => {
axios.get('/ebook-snapshot/get-30-statistic').then((response) => {
const data = response.data;
if (data.success) {
const statisticList = data.content;
init30DayEcharts(statisticList)
}
});
};
const testEcharts = () => {
// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
const option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data:['销量']
},
xAxis: {
data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
};
onMounted(() => {
getStatistic();
// testEcharts();
get30DayStatistic();
});
return {
statistic
}
}
});
</script>
<style scoped>
.tip {
padding: 10px 5px;
margin-bottom: 20px;
border: 1px solid transparent;
background: linear-gradient(white,white) padding-box,repeating-linear-gradient(-45deg, black 0, black 25%, white 0, white 50%) 0/.6em .6em;
animation:ants 12s linear infinite;
}
.tip b{
color: red;
}
</style>
Route - index.ts
- 用于配置路由和登录拦截。
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/home.vue'
import About from '../views/about.vue'
import Aliyun from '../views/aliyun.vue'
import Doc from '../views/doc.vue'
import AdminUser from '../views/admin/admin-user.vue'
import AdminEbook from '../views/admin/admin-ebook.vue'
import AdminCategory from '../views/admin/admin-category.vue'
import AdminDoc from '../views/admin/admin-doc.vue'
import store from "@/store";
import {Tool} from "@/util/tool";
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/doc',
name: 'Doc',
component: Doc
},
{
path: '/about',
name: 'About',
component: About
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/about.vue')
},
{
path: '/aliyun',
name: 'Aliyun',
component: Aliyun
},
{
path: '/admin/user',
name: 'AdminUser',
component: AdminUser,
meta: {
loginRequire: true
}
},
{
path: '/admin/ebook',
name: 'AdminEbook',
component: AdminEbook,
meta: {
loginRequire: true
}
},
{
path: '/admin/category',
name: 'AdminCategory',
component: AdminCategory,
meta: {
loginRequire: true
}
},
{
path: '/admin/doc',
name: 'AdminDoc',
component: AdminDoc,
meta: {
loginRequire: true
}
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
// 路由登录拦截
router.beforeEach((to, from, next) => {
// 要不要对meta.loginRequire属性做监控拦截
if (to.matched.some(function (item) {
console.log(item, "是否需要登录校验:", item.meta.loginRequire);
return item.meta.loginRequire
})) {
const loginUser = store.state.user;
if (Tool.isEmpty(loginUser)) {
console.log("用户未登录!");
next('/');
} else {
next();
}
} else {
next();
}
});
export default router
utils - Tool.ts
- 工具类
export class Tool {
/**
* 空校验 null或""都返回true
*/
public static isEmpty (obj: any) {
if ((typeof obj === 'string')) {
return !obj || obj.replace(/\s+/g, "") === ""
} else {
return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0);
}
}
/**
* 非空校验
*/
public static isNotEmpty (obj: any) {
return !this.isEmpty(obj);
}
/**
* 对象复制
* @param obj
*/
public static copy (obj: object) {
if (Tool.isNotEmpty(obj)) {
return JSON.parse(JSON.stringify(obj));
}
}
/**
* 使用递归将数组转为树形结构
* 父ID属性为parent
*/
public static array2Tree (array: any, parentId: number) {
if (Tool.isEmpty(array)) {
return [];
}
const result = [];
for (let i = 0; i < array.length; i++) {
const c = array[i];
// console.log(Number(c.parent), Number(parentId));
if (Number(c.parent) === Number(parentId)) {
result.push(c);
// 递归查看当前节点对应的子节点
const children = Tool.array2Tree(array, c.id);
if (Tool.isNotEmpty(children)) {
c.children = children;
}
}
}
return result;
}
/**
* 随机生成[len]长度的[radix]进制数
* @param len
* @param radix 默认62
* @returns {string}
*/
public static uuid (len: number, radix = 62) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid = [];
radix = radix || chars.length;
for (let i = 0; i < len; i++) {
uuid[i] = chars[0 | Math.random() * radix];
}
return uuid.join('');
}
}
views
admin-category.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<p>
<a-form layout="inline" :model="param">
<!-- <a-form-item>
<a-button type="primary" @click="handleQuery()">
查询
</a-button>
</a-form-item>-->
<a-form-item>
<a-button type="primary" @click="add()">
新增
</a-button>
</a-form-item>
</a-form>
</p>
<p>
<!-- <a-alert-->
<!-- class="tip"-->
<!-- message="小提示:这里的分类会显示到首页的侧边菜单"-->
<!-- type="info"-->
<!-- closable-->
<!-- />-->
</p>
<a-table
v-if="level1.length > 0"
:columns="columns"
:row-key="record => record.id"
:data-source="level1"
:loading="loading"
:pagination="false"
:defaultExpandAllRows="true"
>
<template #cover="{ text: cover }">
<img v-if="cover" :src="cover" alt="avatar" />
</template>
<template v-slot:action="{ text, record }">
<a-space size="small">
<a-button type="primary" @click="edit(record)">
编辑
</a-button>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="是"
cancel-text="否"
@confirm="handleDelete(record.id)"
>
<a-button type="danger">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-layout-content>
</a-layout>
<a-modal
title="分类表单"
v-model:visible="modalVisible"
:confirm-loading="modalLoading"
@ok="handleModalOk"
>
<a-form :model="category" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="category.name" />
</a-form-item>
<a-form-item label="父分类">
<a-select
v-model:value="category.parent"
ref="select"
>
<a-select-option :value="0">
无
</a-select-option>
<a-select-option v-for="c in level1" :key="c.id" :value="c.id" :disabled="category.id === c.id">
{{c.name}}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="category.sort" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import {Tool} from "@/util/tool";
export default defineComponent({
name: 'AdminCategory',
setup() {
const param = ref();
param.value = {};
const categorys = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
// {
// title: '父分类',
// key: 'parent',
// dataIndex: 'parent'
// },
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级分类树,children属性就是二级分类
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级分类树,children属性就是二级分类
level1.value = [];
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("/category/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
categorys.value = data.content;
console.log("原始数组:", categorys.value);
level1.value = [];
level1.value = Tool.array2Tree(categorys.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
const category = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("/category/save", category.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
category.value = Tool.copy(record);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
category.value = {};
};
const handleDelete = (id: number) => {
axios.delete("/category/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
};
onMounted(() => {
handleQuery();
});
return {
param,
// categorys,
level1,
columns,
loading,
handleQuery,
edit,
add,
category,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
admin-doc.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-row :gutter="24">
<a-col :span="8">
<p>
<a-form layout="inline" :model="param">
<!-- <a-form-item>
<a-button type="primary" @click="handleQuery()">
查询
</a-button>
</a-form-item>-->
<a-form-item>
<a-button type="primary" @click="add()">
新增
</a-button>
</a-form-item>
</a-form>
</p>
<a-table
v-if="level1.length > 0"
:columns="columns"
:row-key="record => record.id"
:data-source="level1"
:loading="loading"
:pagination="false"
size="small"
:defaultExpandAllRows="true"
>
<template #name="{ text, record }">
{{record.sort}} {{text}}
</template>
<template v-slot:action="{ text, record }">
<a-space size="small">
<a-button type="primary" @click="edit(record)" size="small">
编辑
</a-button>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="是"
cancel-text="否"
@confirm="handleDelete(record.id)"
>
<a-button type="danger" size="small">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-col>
<a-col :span="16">
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-button type="primary" @click="handleSave()">
保存
</a-button>
</a-form-item>
</a-form>
</p>
<a-form :model="doc" layout="vertical">
<a-form-item>
<a-input v-model:value="doc.name" placeholder="名称"/>
</a-form-item>
<a-form-item>
<a-tree-select
v-model:value="doc.parent"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeSelectData"
placeholder="请选择父文档"
tree-default-expand-all
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
>
</a-tree-select>
</a-form-item>
<a-form-item>
<a-input v-model:value="doc.sort" placeholder="顺序"/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handlePreviewContent()">
<EyeOutlined /> 内容预览
</a-button>
</a-form-item>
<a-form-item>
<div id="content"></div>
</a-form-item>
</a-form>
</a-col>
</a-row>
<a-drawer width="900" placement="right" :closable="false" :visible="drawerVisible" @close="onDrawerClose">
<div class="wangeditor" :innerHTML="previewHtml"></div>
</a-drawer>
</a-layout-content>
</a-layout>
<!--<a-modal-->
<!-- title="文档表单"-->
<!-- v-model:visible="modalVisible"-->
<!-- :confirm-loading="modalLoading"-->
<!-- @ok="handleModalOk"-->
<!-->-->
<!-- -->
<!--</a-modal>-->
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message, Modal} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined";
import E from 'wangeditor'
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
console.log("路由:", route);
Comments | NOTHING