增加站内消息系统——评论、回复、点赞、系统通知的消息,优化前端。

This commit is contained in:
Himit_ZH 2021-10-05 00:04:46 +08:00
parent 8074bf8e9e
commit 15ba1c69b3
77 changed files with 3634 additions and 337 deletions

View File

@ -10,7 +10,7 @@
## 一、前言
基于前后端分离分布式架构的在线测评平台hoj前端使用vue后端主要使用springbootredismysqlnacos等技术**支持HDU、POJ、Codeforces的vjudge判题同时适配手机端、电脑端浏览。**
基于前后端分离分布式架构的在线测评平台hoj前端使用vue后端主要使用springbootredismysqlnacos等技术**支持HDU、POJ、Codeforces的vjudge判题同时适配手机端、电脑端浏览,拥有讨论区与站内消息系统。**
| 在线Demo | 在线文档 | Github&Gitee仓库地址 | QQ群 |
| :--------------------------------: | :-------------------------------------------------------: | :----------------------------------------------------------: | :-------: |
@ -94,6 +94,7 @@ docker ps # 查看当前运行的容器状态
| 2021-06-25 | 丰富前端操作增加POJ的vjudge判题 | Himit_ZH |
| 2021-08-14 | 增加spj对使用testlib的支持 | Himit_ZH |
| 2021-09-21 | 增加比赛打印功能、账号限制功能 | Himit_ZH |
| 2021-10-05 | 增加站内消息系统——评论、回复、点赞、系统通知的消息,优化前端。 | Himit_ZH |
## 五、部分截图

View File

@ -15,6 +15,7 @@ import top.hcode.hoj.dao.*;
import top.hcode.hoj.pojo.dto.LoginDto;
import top.hcode.hoj.pojo.entity.*;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.impl.SessionServiceImpl;
import top.hcode.hoj.utils.IpUtils;
import top.hcode.hoj.utils.JwtUtils;
@ -37,7 +38,7 @@ public class AdminAccountController {
private UserRoleMapper userRoleDao;
@Autowired
private SessionMapper sessionDao;
private SessionServiceImpl sessionService;
@Autowired
private JwtUtils jwtUtils;
@ -65,8 +66,10 @@ public class AdminAccountController {
response.setHeader("Authorization", jwt); //放到信息头部
response.setHeader("Access-Control-Expose-Headers", "Authorization");
// 会话记录
sessionDao.insert(new Session().setUid(userRoles.getUid())
sessionService.save(new Session().setUid(userRoles.getUid())
.setIp(IpUtils.getUserIpAddr(request)).setUserAgent(request.getHeader("User-Agent")));
// 异步检查是否异地登录
sessionService.checkRemoteLogin(userRoles.getUid());
return CommonResult.successResponse(MapUtil.builder()
.put("uid", userRoles.getUid())
.put("username", userRoles.getUsername())

View File

@ -382,6 +382,7 @@ public class AdminContestController {
public CommonResult setContestProblem(@RequestBody ContestProblem contestProblem) {
boolean result = contestProblemService.saveOrUpdate(contestProblem);
if (result) {
contestProblemService.syncContestRecord(contestProblem.getPid(), contestProblem.getCid(), contestProblem.getDisplayId());
return CommonResult.successResponse(contestProblem, "更新成功!");
} else {
return CommonResult.errorResponse("更新失败", CommonResult.STATUS_FAIL);

View File

@ -4,6 +4,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
@ -17,13 +18,20 @@ import top.hcode.hoj.pojo.entity.UserInfo;
import top.hcode.hoj.pojo.entity.UserRecord;
import top.hcode.hoj.pojo.entity.UserRole;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.AdminSysNoticeService;
import top.hcode.hoj.service.UserInfoService;
import top.hcode.hoj.service.UserRecordService;
import top.hcode.hoj.service.impl.UserRoleServiceImpl;
import top.hcode.hoj.utils.RedisUtils;
import top.hcode.hoj.utils.ShiroUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.*;
import java.util.stream.Collectors;
import static oshi.util.GlobalConfig.set;
/**
* @Author: Himit_ZH
@ -46,6 +54,9 @@ public class AdminUserController {
@Autowired
private RedisUtils redisUtils;
@Resource
private AdminSysNoticeService adminSysNoticeService;
@GetMapping("/get-user-list")
@RequiresAuthentication
@ -72,7 +83,8 @@ public class AdminUserController {
@RequiresPermissions("user_admin")
@RequiresAuthentication
@Transactional
public CommonResult editUser(@RequestBody Map<String, Object> params) {
public CommonResult editUser(@RequestBody Map<String, Object> params,
HttpServletRequest request) {
String username = (String) params.get("username");
String uid = (String) params.get("uid");
String realname = (String) params.get("realname");
@ -91,9 +103,15 @@ public class AdminUserController {
.set("status", status);
boolean result1 = userInfoService.update(updateWrapper1);
UpdateWrapper<UserRole> updateWrapper2 = new UpdateWrapper<>();
updateWrapper2.eq("uid", uid).set("role_id", type);
boolean result2 = userRoleService.update(updateWrapper2);
QueryWrapper<UserRole> userRoleQueryWrapper = new QueryWrapper<>();
userRoleQueryWrapper.eq("uid", uid);
UserRole userRole = userRoleService.getOne(userRoleQueryWrapper, false);
boolean result2 = false;
int oldType = userRole.getRoleId().intValue();
if (userRole.getRoleId().intValue() != type) {
userRole.setRoleId(Long.valueOf(type));
result2 = userRoleService.updateById(userRole);
}
if (result1) {
// 需要重新登录
userRoleService.deleteCache(uid, true);
@ -102,10 +120,16 @@ public class AdminUserController {
userRoleService.deleteCache(uid, false);
}
if (result1 && result2) {
return CommonResult.successResponse(null, "修改成功!");
if (result2) {
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
String title = "权限变更通知(Authority Change Notice)";
String content = userRoleService.getAuthChangeContent(oldType, type);
adminSysNoticeService.addSingleNoticeToUser(userRolesVo.getUid(), uid, title, content, "Sys");
}
return CommonResult.errorResponse("修改失败!");
return CommonResult.successResponse(null, "修改成功!");
}
@DeleteMapping("/delete-user")
@ -148,6 +172,9 @@ public class AdminUserController {
boolean result2 = userRoleService.saveBatch(userRoleList);
boolean result3 = userRecordService.saveBatch(userRecordList);
if (result1 && result2 && result3) {
// 异步同步系统通知
List<String> uidList = userInfoList.stream().map(UserInfo::getUuid).collect(Collectors.toList());
adminSysNoticeService.syncNoticeToNewRegisterBatchUser(uidList);
return CommonResult.successResponse(null, "添加成功!");
} else {
return CommonResult.errorResponse("删除失败");
@ -193,6 +220,9 @@ public class AdminUserController {
if (result1 && result2 && result3) {
String key = IdUtil.simpleUUID();
redisUtils.hmset(key, userInfo, 1800); // 存储半小时
// 异步同步系统通知
List<String> uidList = userInfoList.stream().map(UserInfo::getUuid).collect(Collectors.toList());
adminSysNoticeService.syncNoticeToNewRegisterBatchUser(uidList);
return CommonResult.successResponse(MapUtil.builder()
.put("key", key).map(), "生成指定用户成功!");
} else {

View File

@ -0,0 +1,79 @@
package top.hcode.hoj.controller.msg;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.entity.AdminSysNotice;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.impl.AdminSysNoticeServiceImpl;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:38
* @Description: 负责管理员发送系统通知
*/
@RestController
@RequestMapping("/api/admin/msg")
public class AdminSysNoticeController {
@Resource
private AdminSysNoticeServiceImpl adminSysNoticeService;
@GetMapping("/notice")
@RequiresAuthentication
@RequiresRoles("root")
public CommonResult getSysNotice(@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
@RequestParam(value = "type", required = false) String type) {
// 页数每页题数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 5;
return CommonResult.successResponse(adminSysNoticeService.getSysNotice(limit, currentPage, type));
}
@PostMapping("/notice")
@RequiresAuthentication
@RequiresRoles("root")
public CommonResult addSysNotice(@RequestBody AdminSysNotice adminSysNotice) {
boolean isOk = adminSysNoticeService.saveOrUpdate(adminSysNotice);
if (isOk) {
return CommonResult.successResponse("发布成功");
} else {
return CommonResult.errorResponse("发布失败");
}
}
@DeleteMapping("/notice")
@RequiresAuthentication
@RequiresRoles("root")
public CommonResult deleteSysNotice(@RequestParam("id") Long id) {
boolean isOk = adminSysNoticeService.removeById(id);
if (isOk) {
return CommonResult.successResponse("删除成功");
} else {
return CommonResult.errorResponse("删除失败");
}
}
@PutMapping("/notice")
@RequiresAuthentication
@RequiresRoles("root")
public CommonResult updateSysNotice(@RequestBody AdminSysNotice adminSysNotice) {
boolean isOk = adminSysNoticeService.saveOrUpdate(adminSysNotice);
if (isOk) {
return CommonResult.successResponse("更新成功");
} else {
return CommonResult.errorResponse("更新失败");
}
}
}

View File

@ -0,0 +1,151 @@
package top.hcode.hoj.controller.msg;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.pojo.vo.UserUnreadMsgCountVo;
import top.hcode.hoj.service.impl.MsgRemindServiceImpl;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:40
* @Description: 获取用户 评论我的回复我的收到的赞的消息
*/
@RestController
@RequestMapping("/api/msg")
public class MsgRemindController {
@Resource
private MsgRemindServiceImpl msgRemindService;
/**
* @MethodName getUnreadMsgCount
* @Description 获取用户的未读消息数量包括评论我的回复我的收到的赞系统通知我的消息
* @Return
* @Since 2021/10/1
*/
@RequestMapping(value = "/unread", method = RequestMethod.GET)
@RequiresAuthentication
public CommonResult getUnreadMsgCount(HttpServletRequest request) {
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
UserUnreadMsgCountVo userUnreadMsgCount = msgRemindService.getUserUnreadMsgCount(userRolesVo.getUid());
if (userUnreadMsgCount == null) {
userUnreadMsgCount = new UserUnreadMsgCountVo(0, 0, 0, 0, 0);
}
return CommonResult.successResponse(userUnreadMsgCount);
}
/**
* @param type Discuss Reply Like Sys Mine
* @param request
* @MethodName cleanMsg
* @Description 根据type清空各个消息模块的消息或单个消息
* @Return
* @Since 2021/10/3
*/
@RequestMapping(value = "/clean", method = RequestMethod.DELETE)
@RequiresAuthentication
public CommonResult cleanMsg(@RequestParam("type") String type,
@RequestParam(value = "id", required = false) Long id,
HttpServletRequest request) {
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
boolean isOk = msgRemindService.cleanMsgByType(type, id, userRolesVo.getUid());
if (isOk) {
return CommonResult.successResponse(null, "清空全部成功");
} else {
return CommonResult.errorResponse("清空失败");
}
}
/**
* @param limit
* @param currentPage
* @MethodName getCommentMsg
* @Description 获取评论我的讨论贴的消息按未读的在前时间晚的在前进行排序
* @Return
* @Since 2021/10/1
*/
@RequestMapping(value = "/comment", method = RequestMethod.GET)
@RequiresAuthentication
public CommonResult getCommentMsg(@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
HttpServletRequest request) {
// 页数每页题数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 5;
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
return CommonResult.successResponse(msgRemindService.getUserMsgList(userRolesVo.getUid(), "Discuss", limit, currentPage));
}
/**
* @param limit
* @param currentPage
* @MethodName getReplyMsg
* @Description 获取回复我的评论的消息按未读的在前时间晚的在前进行排序
* @Return
* @Since 2021/10/1
*/
@RequestMapping(value = "/reply", method = RequestMethod.GET)
@RequiresAuthentication
public CommonResult getReplyMsg(@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
HttpServletRequest request) {
// 页数每页题数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 5;
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
return CommonResult.successResponse(msgRemindService.getUserMsgList(userRolesVo.getUid(), "Reply", limit, currentPage));
}
/**
* @param limit
* @param currentPage
* @MethodName getLikeMsg
* @Description 获取点赞我的的消息按未读的在前时间晚的在前进行排序
* @Return
* @Since 2021/10/1
*/
@RequestMapping(value = "/like", method = RequestMethod.GET)
@RequiresAuthentication
public CommonResult getLikeMsg(@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
HttpServletRequest request) {
// 页数每页题数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 5;
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
return CommonResult.successResponse(msgRemindService.getUserMsgList(userRolesVo.getUid(), "Like", limit, currentPage));
}
}

View File

@ -0,0 +1,60 @@
package top.hcode.hoj.controller.msg;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.impl.UserSysNoticeServiceImpl;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:42
* @Description: 负责用户的 系统消息模块我的消息模块
*/
@RestController
@RequestMapping("/api/msg")
public class UserSysNoticeController {
@Resource
private UserSysNoticeServiceImpl userSysNoticeService;
@RequestMapping(value = "/sys", method = RequestMethod.GET)
@RequiresAuthentication
public CommonResult getSysNotice(@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
HttpServletRequest request) {
// 页数每页题数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 5;
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
return CommonResult.successResponse(userSysNoticeService.getSysNotice(limit, currentPage, userRolesVo.getUid()));
}
@RequestMapping(value = "/mine", method = RequestMethod.GET)
@RequiresAuthentication
public CommonResult getMineNotice(@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
HttpServletRequest request) {
// 页数每页题数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 5;
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
return CommonResult.successResponse(userSysNoticeService.getMineNotice(limit, currentPage, userRolesVo.getUid()));
}
}

View File

@ -31,6 +31,7 @@ import top.hcode.hoj.utils.IpUtils;
import top.hcode.hoj.utils.JwtUtils;
import top.hcode.hoj.utils.RedisUtils;
import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -72,11 +73,14 @@ public class AccountController {
private ProblemServiceImpl problemService;
@Autowired
private SessionMapper sessionDao;
private SessionServiceImpl sessionService;
@Autowired
private ConfigVo configVo;
@Resource
private AdminSysNoticeServiceImpl adminSysNoticeService;
/**
* @MethodName getRegisterCode
* @Params * @param null
@ -89,7 +93,7 @@ public class AccountController {
if (!configVo.getRegister()) { // 需要判断一下网站是否开启注册
return CommonResult.errorResponse("对不起!本站暂未开启注册功能!", CommonResult.STATUS_ACCESS_DENIED);
}
if (!emailService.isOk()){
if (!emailService.isOk()) {
return CommonResult.errorResponse("对不起!本站邮箱系统未配置,暂不支持注册!", CommonResult.STATUS_ACCESS_DENIED);
}
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
@ -164,7 +168,7 @@ public class AccountController {
if (StringUtils.isEmpty(captcha) || StringUtils.isEmpty(email) || StringUtils.isEmpty(captchaKey)) {
return CommonResult.errorResponse("邮箱或验证码不能为空");
}
if (!emailService.isOk()){
if (!emailService.isOk()) {
return CommonResult.errorResponse("对不起!本站邮箱系统未配置,暂不支持重置密码!", CommonResult.STATUS_ACCESS_DENIED);
}
// 获取redis中的验证码
@ -264,6 +268,7 @@ public class AccountController {
int result3 = userRecordDao.insert(new UserRecord().setUid(uuid));
if (result1 == 1 && result2 == 1 && result3 == 1) {
redisUtils.del(registerDto.getEmail());
adminSysNoticeService.syncNoticeToNewRegisterUser(uuid);
return CommonResult.successResponse(null, "谢谢你的注册!");
} else {
return CommonResult.errorResponse("注册失败!", CommonResult.STATUS_ERROR); // 插入数据库失败返回500
@ -304,10 +309,12 @@ public class AccountController {
response.setHeader("Access-Control-Expose-Headers", "Authorization");
// 会话记录
sessionDao.insert(new Session()
sessionService.save(new Session()
.setUid(userRoles.getUid())
.setIp(IpUtils.getUserIpAddr(request))
.setUserAgent(request.getHeader("User-Agent")));
// 异步检查是否异地登录
sessionService.checkRemoteLogin(userRoles.getUid());
return CommonResult.successResponse(MapUtil.builder()
.put("uid", userRoles.getUid())
@ -352,7 +359,8 @@ public class AccountController {
* @Since 2021/01/07
*/
@GetMapping("/get-user-home-info")
public CommonResult getUserHomeInfo(@RequestParam(value = "uid", required = false) String uid, HttpServletRequest request) {
public CommonResult getUserHomeInfo(@RequestParam(value = "uid", required = false) String uid,
HttpServletRequest request) {
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 如果没有uid默认查询当前登录用户的
@ -360,6 +368,9 @@ public class AccountController {
uid = userRolesVo.getUid();
}
UserHomeVo userHomeInfo = userRecordDao.getUserHomeInfo(uid);
if (userHomeInfo == null) {
return CommonResult.errorResponse("用户不存在");
}
QueryWrapper<UserAcproblem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("uid", uid).select("distinct pid");
List<Long> pidList = new LinkedList<>();
@ -383,7 +394,7 @@ public class AccountController {
QueryWrapper<Session> sessionQueryWrapper = new QueryWrapper<>();
sessionQueryWrapper.eq("uid", uid).orderByDesc("gmt_create").last("limit 1");
Session recentSession = sessionDao.selectOne(sessionQueryWrapper);
Session recentSession = sessionService.getOne(sessionQueryWrapper, false);
if (recentSession != null) {
userHomeInfo.setRecentLoginTime(recentSession.getGmtCreate());
}

View File

@ -102,6 +102,7 @@ public class CommentController {
@PostMapping("/comment")
@RequiresPermissions("comment_add")
@RequiresAuthentication
@Transactional
public CommonResult addComment(@RequestBody Comment comment, HttpServletRequest request) {
// 获取当前登录的用户
HttpSession session = request.getSession();
@ -137,10 +138,17 @@ public class CommentController {
commentsVo.setReplyList(new LinkedList<>());
// 如果是讨论区的回复发布成功需要添加统计该讨论的回复数
if (comment.getDid() != null) {
UpdateWrapper<Discussion> discussionUpdateWrapper = new UpdateWrapper<>();
discussionUpdateWrapper.eq("id", comment.getDid())
.setSql("comment_num=comment_num+1");
discussionService.update(discussionUpdateWrapper);
Discussion discussion = discussionService.getById(comment.getDid());
if (discussion != null) {
discussion.setCommentNum(discussion.getCommentNum() + 1);
discussionService.updateById(discussion);
// 更新消息
commentService.updateCommentMsg(discussion.getUid(),
userRolesVo.getUid(),
comment.getContent(),
comment.getDid());
}
}
return CommonResult.successResponse(commentsVo, "评论成功");
} else {
@ -192,6 +200,8 @@ public class CommentController {
@Transactional
public CommonResult addDiscussionLike(@RequestParam("cid") Integer cid,
@RequestParam("toLike") Boolean toLike,
@RequestParam("sourceId") Integer sourceId,
@RequestParam("sourceType") String sourceType,
HttpServletRequest request) {
// 获取当前登录的用户
@ -213,9 +223,12 @@ public class CommentController {
}
}
// 点赞+1
UpdateWrapper<Comment> commentUpdateWrapper = new UpdateWrapper<>();
commentUpdateWrapper.setSql("like_num=like_num+1").eq("id", cid);
commentService.update(commentUpdateWrapper);
Comment comment = commentService.getById(cid);
if (comment != null) {
comment.setLikeNum(comment.getLikeNum() + 1);
commentService.updateById(comment);
commentService.updateCommentLikeMsg(comment.getFromUid(), userRolesVo.getUid(), sourceId, sourceType);
}
return CommonResult.successResponse(null, "点赞成功");
} else { // 取消点赞
if (commentLike != null) { // 如果存在就删除
@ -280,6 +293,14 @@ public class CommentController {
discussionUpdateWrapper.eq("id", replyDto.getDid())
.setSql("comment_num=comment_num+1");
discussionService.update(discussionUpdateWrapper);
// 更新消息
replyService.updateReplyMsg(replyDto.getDid(),
"Discussion",
reply.getContent(),
replyDto.getQuoteId(),
replyDto.getQuoteType(),
reply.getToUid(),
reply.getFromUid());
}
return CommonResult.successResponse(reply, "评论成功");
} else {

View File

@ -300,10 +300,16 @@ public class ContestController {
return CommonResult.errorResponse("该比赛题目当前不可访问!", CommonResult.STATUS_FORBIDDEN);
}
// 设置比赛题目的标题为设置展示标题
problem.setTitle(contestProblem.getDisplayTitle());
List<String> tagsStr = new LinkedList<>();
// 比赛结束后才开放标签和source
// 比赛结束后才开放标签和source出题人难度
if (contest.getStatus().intValue() != Constants.Contest.STATUS_ENDED.getCode()) {
problem.setSource(null);
problem.setAuthor(null);
problem.setDifficulty(null);
QueryWrapper<ProblemTag> problemTagQueryWrapper = new QueryWrapper<>();
problemTagQueryWrapper.eq("pid", contestProblem.getPid());
// 获取该题号对应的标签id
@ -361,11 +367,6 @@ public class ContestController {
LangNameAndCode.put(tmpMap.get(codeTemplate.getLid()), codeTemplate.getCode());
}
}
if (DateUtil.isIn(now, contest.getStartTime(), contest.getEndTime())) {
// 比赛过程中隐藏出题人
problem.setAuthor(null);
}
// 将数据统一写入到一个Vo返回数据实体类中
ProblemInfoVo problemInfoVo = new ProblemInfoVo(problem, tagsStr, languagesStr, problemCount, LangNameAndCode);
return CommonResult.successResponse(problemInfoVo, "获取成功");

View File

@ -227,9 +227,13 @@ public class DiscussionController {
}
}
// 点赞+1
UpdateWrapper<Discussion> discussionUpdateWrapper = new UpdateWrapper<>();
discussionUpdateWrapper.setSql("like_num=like_num+1").eq("id", did);
discussionService.update(discussionUpdateWrapper);
Discussion discussion = discussionService.getById(did);
if (discussion != null) {
discussion.setLikeNum(discussion.getLikeNum() + 1);
discussionService.updateById(discussion);
// 更新点赞消息
discussionService.updatePostLikeMsg(discussion.getUid(), userRolesVo.getUid(), did);
}
return CommonResult.successResponse(null, "点赞成功");
} else { // 取消点赞
if (discussionLike != null) { // 如果存在就删除

View File

@ -15,7 +15,7 @@ import top.hcode.hoj.utils.JsoupUtils;
*/
public class HDUProblemStrategy extends ProblemStrategy {
public static final String JUDGE_NAME = "HDU";
public static final String HOST = "https://acm.hdu.edu.cn";
public static final String HOST = "https://acm.dingbacode.com";
public static final String PROBLEM_URL = "/showproblem.php?pid=%s";
/**

View File

@ -0,0 +1,16 @@
package top.hcode.hoj.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import top.hcode.hoj.pojo.entity.AdminSysNotice;
import top.hcode.hoj.pojo.vo.AdminSysNoticeVo;
@Mapper
@Repository
public interface AdminSysNoticeMapper extends BaseMapper<AdminSysNotice> {
IPage<AdminSysNoticeVo> getAdminSysNotice(Page<AdminSysNoticeVo> page, @Param("type") String type);
}

View File

@ -0,0 +1,20 @@
package top.hcode.hoj.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import top.hcode.hoj.pojo.entity.MsgRemind;
import top.hcode.hoj.pojo.vo.UserMsgVo;
import top.hcode.hoj.pojo.vo.UserUnreadMsgCountVo;
@Mapper
@Repository
public interface MsgRemindMapper extends BaseMapper<MsgRemind> {
UserUnreadMsgCountVo getUserUnreadMsgCount(@Param("uid") String uid);
IPage<UserMsgVo> getUserMsg(Page<UserMsgVo> page, @Param("uid") String uid,
@Param("action") String action);
}

View File

@ -0,0 +1,17 @@
package top.hcode.hoj.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import top.hcode.hoj.pojo.entity.UserSysNotice;
import top.hcode.hoj.pojo.vo.SysMsgVo;
@Mapper
@Repository
public interface UserSysNoticeMapper extends BaseMapper<UserSysNotice> {
IPage<SysMsgVo> getSysOrMineNotice(Page<SysMsgVo> page, @Param("uid") String uid, @Param("type") String type);
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.AdminSysNoticeMapper">
<resultMap id="map_AdminSysNoticeList" type="top.hcode.hoj.pojo.vo.AdminSysNoticeVo">
<id column="id" property="id"></id>
<result column="title" property="title"></result>
<result column="content" property="content"></result>
<result column="type" property="type"></result>
<result column="state" property="state"></result>
<result column="username" property="adminUsername"></result>
<result column="gmt_create" property="gmtCreate"></result>
</resultMap>
<select id="getAdminSysNotice" resultMap="map_AdminSysNoticeList">
select
a.id as id,
a.title as title,
a.content as content,
a.type as type,
a.state as state,
a.gmt_create as gmt_create,
u.username as username
from admin_sys_notice a, user_info u
<where>
a.admin_id = u.uuid
<if test="type!=null">
and a.type = #{type}
</if>
</where>
order by a.state asc, a.gmt_create desc
</select>
</mapper>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.MsgRemindMapper">
<select id="getUserUnreadMsgCount" resultType="top.hcode.hoj.pojo.vo.UserUnreadMsgCountVo" useCache="true">
SELECT
(SELECT COUNT(1) FROM msg_remind WHERE recipient_id=#{uid} AND state=0 AND `action`='Discuss') AS 'comment',
(SELECT COUNT(1) FROM msg_remind WHERE recipient_id=#{uid} AND state=0 AND `action`='Reply') AS 'reply',
(SELECT COUNT(1) FROM msg_remind WHERE recipient_id=#{uid} AND state=0 AND `action` LIKE 'Like%') AS 'like',
(SELECT COUNT(1) FROM user_sys_notice WHERE recipient_id=#{uid} AND state=0 AND `type`='Sys') AS 'sys',
(SELECT COUNT(1) FROM user_sys_notice WHERE recipient_id=#{uid} AND state=0 AND `type`='Mine') AS 'mine'
</select>
<resultMap id="map_UserMsgList" type="top.hcode.hoj.pojo.vo.UserMsgVo">
<id column="id" property="id"></id>
<result column="sender_id" property="senderId"></result>
<result column="action" property="action"></result>
<result column="source_type" property="sourceType"></result>
<result column="source_id" property="sourceId"></result>
<result column="source_content" property="sourceContent"></result>
<result column="quote_id" property="quoteId"></result>
<result column="quote_type" property="quoteType"></result>
<result column="url" property="url"></result>
<result column="state" property="state"></result>
<result column="gmt_create" property="gmtCreate"></result>
<result column="username" property="senderUsername"></result>
<result column="avatar" property="senderAvatar"></result>
</resultMap>
<select id="getUserMsg" resultMap="map_UserMsgList">
select
m.id as id,
m.sender_id as sender_id,
m.action as 'action',
m.source_id as source_id,
m.source_type as source_type,
m.source_content as source_content,
m.quote_id as quote_id,
m.quote_type as quote_type,
m.url as url,
m.state as state,
m.gmt_create as gmt_create,
u.username as username,
u.avatar as avatar
from msg_remind m,user_info u
<where>
m.sender_id = u.uuid
and m.recipient_id = #{uid}
<choose>
<when test="action == 'Like'">
and (m.action = 'Like_Post' OR m.action = 'Like_Discuss')
</when>
<otherwise>
and m.action = #{action}
</otherwise>
</choose>
</where>
order by m.state asc, m.gmt_create desc
</select>
</mapper>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.UserSysNoticeMapper">
<resultMap id="map_SysMsgList" type="top.hcode.hoj.pojo.vo.SysMsgVo">
<id column="id" property="id"></id>
<result column="type" property="type"></result>
<result column="state" property="state"></result>
<result column="gmt_create" property="gmtCreate"></result>
<result column="title" property="title"></result>
<result column="content" property="content"></result>
<result column="admin_id" property="adminId"></result>
</resultMap>
<select id="getSysOrMineNotice" resultMap="map_SysMsgList">
select
u.id as id,
u.type as type,
u.state as state,
a.gmt_create as gmt_create,
a.title as title,
a.content as content,
a.admin_id as admin_id
from user_sys_notice u,admin_sys_notice a
<where>
u.sys_notice_id = a.id
<if test="uid!=null">
and u.recipient_id = #{uid}
</if>
<if test="type!=null">
and u.type = #{type}
</if>
</where>
order by u.state asc,a.gmt_create desc
</select>
</mapper>

View File

@ -15,5 +15,9 @@ public class ReplyDto {
private Reply reply;
private Long did;
private Integer did;
private Integer quoteId;
private String quoteType;
}

View File

@ -0,0 +1,38 @@
package top.hcode.hoj.pojo.vo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* @Author: Himit_ZH
* @Date: 2021/10/4 14:03
* @Description:
*/
@Data
@ApiModel(value="系统通知消息", description="")
public class AdminSysNoticeVo {
private Long id;
@ApiModelProperty(value = "通知标题")
private String title;
@ApiModelProperty(value = "通知内容")
private String content;
@ApiModelProperty(value = "发给哪些用户类型,例如全部用户All指定单个用户Single管理员Admin")
private String type;
@ApiModelProperty(value = "是否已被拉取过,如果已经拉取过,就无需再次拉取")
private Boolean state;
@ApiModelProperty(value = "发布通知的管理员用户名")
private String adminUsername;
private Date gmtCreate;
}

View File

@ -0,0 +1,38 @@
package top.hcode.hoj.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* @Author: Himit_ZH
* @Date: 2021/10/3 16:36
* @Description:
*/
@ApiModel(value="用户的系统消息", description="")
@Data
public class SysMsgVo {
private Long id;
@ApiModelProperty(value = "通知标题")
private String title;
@ApiModelProperty(value = "通知内容")
private String content;
@ApiModelProperty(value = "发布通知的管理员id")
private String adminId;
@ApiModelProperty(value = "消息类型系统通知Sys、我的信息Mine")
private String type;
@ApiModelProperty(value = "是否已读")
private Boolean state;
private Date gmtCreate;
}

View File

@ -0,0 +1,63 @@
package top.hcode.hoj.pojo.vo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* @Author: Himit_ZH
* @Date: 2021/10/2 20:50
* @Description:
*/
@ApiModel(value="用户的讨论贴被评论的、被点赞、评论被回复的消息Vo", description="")
@Data
public class UserMsgVo {
private Long id;
@ApiModelProperty(value = "动作类型如点赞讨论帖Like_Post、点赞评论Like_Discuss、评论Discuss、回复Reply等")
private String action;
@ApiModelProperty(value = "消息来源id讨论id或比赛id")
private Integer sourceId;
@ApiModelProperty(value = "事件源类型:'Discussion'、'Contest'等")
private String sourceType;
@ApiModelProperty(value = "事件源的标题,讨论帖子的标题或者比赛的标题")
private String sourceTitle;
@ApiModelProperty(value = "事件源的内容,比如回复的内容,回复的评论等等,不超过250字符超过使用...")
private String sourceContent;
@ApiModelProperty(value = "事件引用上一级评论或回复id")
private Integer quoteId;
@ApiModelProperty(value = "事件引用上一级的类型Comment、Reply")
private String quoteType;
@ApiModelProperty(value = "事件引用上一级的内容,例如回复你的源评论内容")
private String quoteContent;
@ApiModelProperty(value = "事件所发生的地点链接 url")
private String url;
@ApiModelProperty(value = "是否已读")
private Boolean state;
@ApiModelProperty(value = "动作发出者的uid")
private String senderId;
@ApiModelProperty(value = "动作发出者的用户名")
private String senderUsername;
@ApiModelProperty(value = "动作发出者的头像")
private String senderAvatar;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
}

View File

@ -0,0 +1,34 @@
package top.hcode.hoj.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:59
* @Description:
*/
@ApiModel(value="用户未读消息统计", description="")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserUnreadMsgCountVo {
@ApiModelProperty(value = "未读评论")
private Integer comment;
@ApiModelProperty(value = "未读回复")
private Integer reply;
@ApiModelProperty(value = "未读点赞")
private Integer like;
@ApiModelProperty(value = "未读系统通知")
private Integer sys;
@ApiModelProperty(value = "未读我的消息")
private Integer mine;
}

View File

@ -1,4 +1,4 @@
package top.hcode.hoj.service;
package top.hcode.hoj.schedule;
public interface ScheduleService {
void deleteAvatar();
@ -12,4 +12,6 @@ public interface ScheduleService {
void getCodeforcesRating();
void deleteUserSession();
void syncNoticeToRecentHalfYearUser();
}

View File

@ -1,4 +1,4 @@
package top.hcode.hoj.service.impl;
package top.hcode.hoj.schedule;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
@ -12,25 +12,18 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import top.hcode.hoj.pojo.entity.File;
import top.hcode.hoj.pojo.entity.Session;
import top.hcode.hoj.pojo.entity.UserInfo;
import top.hcode.hoj.pojo.entity.UserRecord;
import top.hcode.hoj.service.ScheduleService;
import top.hcode.hoj.pojo.entity.*;
import top.hcode.hoj.service.UserInfoService;
import top.hcode.hoj.service.UserRecordService;
import top.hcode.hoj.service.impl.*;
import top.hcode.hoj.utils.Constants;
import top.hcode.hoj.utils.JsoupUtils;
import top.hcode.hoj.utils.RedisUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -76,6 +69,13 @@ public class ScheduleServiceImpl implements ScheduleService {
@Resource
private SessionServiceImpl sessionService;
@Resource
private AdminSysNoticeServiceImpl adminSysNoticeService;
@Resource
private UserSysNoticeServiceImpl userSysNoticeService;
/**
* @MethodName deleteAvatar
* @Params * @param null
@ -265,7 +265,7 @@ public class ScheduleServiceImpl implements ScheduleService {
/**
* @MethodName deleteUserSession
* @Params * @param null
* @Description 每天3点定时删除用户三个月前的session表记录
* @Description 每天3点定时删除用户半年的session表记录
* @Return
* @Since 2021/9/6
*/
@ -274,7 +274,7 @@ public class ScheduleServiceImpl implements ScheduleService {
public void deleteUserSession() {
QueryWrapper<Session> sessionQueryWrapper = new QueryWrapper<>();
DateTime dateTime = DateUtil.offsetMonth(new Date(), -3);
DateTime dateTime = DateUtil.offsetMonth(new Date(), -6);
String threeMonthsBeforeDate = dateTime.toString("yyyy-MM-dd HH:mm:ss");
sessionQueryWrapper.select("distinct uid");
sessionQueryWrapper.apply("UNIX_TIMESTAMP(gmt_create) >= UNIX_TIMESTAMP('{0}')", threeMonthsBeforeDate);
@ -288,9 +288,70 @@ public class ScheduleServiceImpl implements ScheduleService {
boolean isSuccess = sessionService.remove(sessionUpdateWrapper);
if (!isSuccess) {
log.error("=============数据库session表定时删除用户个月前的记录失败===============");
log.error("=============数据库session表定时删除用户6个月前的记录失败===============");
}
}
}
/**
* @MethodName syncNoticeToUser
* @Description 每一小时拉取系统通知表admin_sys_notice到表user_sys_notice(只推送给半年内有登录过的用户)
* @Return
* @Since 2021/10/3
*/
@Override
@Scheduled(cron = "0 0 0/1 * * *")
public void syncNoticeToRecentHalfYearUser() {
QueryWrapper<AdminSysNotice> adminSysNoticeQueryWrapper = new QueryWrapper<>();
adminSysNoticeQueryWrapper.eq("state", false);
List<AdminSysNotice> adminSysNotices = adminSysNoticeService.list(adminSysNoticeQueryWrapper);
if (adminSysNotices.size() == 0) {
return;
}
QueryWrapper<Session> sessionQueryWrapper = new QueryWrapper<>();
sessionQueryWrapper.select("DISTINCT uid");
List<Session> sessionList = sessionService.list(sessionQueryWrapper);
List<String> userIds = sessionList.stream().map(Session::getUid).collect(Collectors.toList());
for (AdminSysNotice adminSysNotice : adminSysNotices) {
switch (adminSysNotice.getType()) {
case "All":
List<UserSysNotice> userSysNoticeList = new ArrayList<>();
for (String uid : userIds) {
UserSysNotice userSysNotice = new UserSysNotice();
userSysNotice.setRecipientId(uid)
.setType("Sys")
.setSysNoticeId(adminSysNotice.getId());
userSysNoticeList.add(userSysNotice);
}
boolean isOk1 = userSysNoticeService.saveOrUpdateBatch(userSysNoticeList);
if (isOk1) {
adminSysNotice.setState(true);
}
break;
case "Single":
UserSysNotice userSysNotice = new UserSysNotice();
userSysNotice.setRecipientId(adminSysNotice.getRecipientId())
.setType("Mine")
.setSysNoticeId(adminSysNotice.getId());
boolean isOk2 = userSysNoticeService.saveOrUpdate(userSysNotice);
if (isOk2) {
adminSysNotice.setState(true);
}
break;
case "Admin":
break;
}
}
boolean isUpdateNoticeOk = adminSysNoticeService.saveOrUpdateBatch(adminSysNotices);
if (!isUpdateNoticeOk) {
log.error("=============推送系统通知更新状态失败===============");
}
}
}

View File

@ -0,0 +1,23 @@
package top.hcode.hoj.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import top.hcode.hoj.pojo.entity.AdminSysNotice;
import top.hcode.hoj.pojo.vo.AdminSysNoticeVo;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:33
* @Description:
*/
public interface AdminSysNoticeService extends IService<AdminSysNotice> {
public IPage<AdminSysNoticeVo> getSysNotice(int limit,int currentPage,String type);
public void syncNoticeToNewRegisterUser(String uid);
public void syncNoticeToNewRegisterBatchUser(List<String> uidList);
public void addSingleNoticeToUser(String adminId, String recipientId, String title, String content,String type);
}

View File

@ -0,0 +1,21 @@
package top.hcode.hoj.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import top.hcode.hoj.pojo.entity.MsgRemind;
import top.hcode.hoj.pojo.vo.UserMsgVo;
import top.hcode.hoj.pojo.vo.UserUnreadMsgCountVo;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:32
* @Description:
*/
public interface MsgRemindService extends IService<MsgRemind> {
UserUnreadMsgCountVo getUserUnreadMsgCount(String uid);
boolean cleanMsgByType(String type, Long id, String uid);
IPage<UserMsgVo> getUserMsgList(String uid, String action, int limit, int currentPage);
}

View File

@ -4,4 +4,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
import top.hcode.hoj.pojo.entity.Session;
public interface SessionService extends IService<Session> {
public void checkRemoteLogin(String uid);
}

View File

@ -16,4 +16,6 @@ import top.hcode.hoj.pojo.vo.UserRolesVo;
public interface UserRoleService extends IService<UserRole> {
IPage<UserRolesVo> getUserList(int limit, int currentPage, String keyword,Boolean onlyAdmin);
void deleteCache(String uid, boolean isRemoveSession);
String getAuthChangeContent(int oldType,int newType);
}

View File

@ -0,0 +1,12 @@
package top.hcode.hoj.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import top.hcode.hoj.pojo.entity.UserSysNotice;
import top.hcode.hoj.pojo.vo.SysMsgVo;
public interface UserSysNoticeService extends IService<UserSysNotice> {
IPage<SysMsgVo> getSysNotice(int limit, int currentPage, String uid);
IPage<SysMsgVo> getMineNotice(int limit, int currentPage, String uid);
}

View File

@ -0,0 +1,110 @@
package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.hcode.hoj.dao.AdminSysNoticeMapper;
import top.hcode.hoj.pojo.entity.AdminSysNotice;
import top.hcode.hoj.pojo.entity.UserSysNotice;
import top.hcode.hoj.pojo.vo.AdminSysNoticeVo;
import top.hcode.hoj.service.AdminSysNoticeService;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:34
* @Description:
*/
@Service
public class AdminSysNoticeServiceImpl extends ServiceImpl<AdminSysNoticeMapper, AdminSysNotice> implements AdminSysNoticeService {
@Resource
private AdminSysNoticeMapper adminSysNoticeMapper;
@Resource
private UserSysNoticeServiceImpl userSysNoticeService;
@Override
public IPage<AdminSysNoticeVo> getSysNotice(int limit, int currentPage, String type) {
Page<AdminSysNoticeVo> page = new Page<>(currentPage, limit);
return adminSysNoticeMapper.getAdminSysNotice(page, type);
}
@Override
@Async
public void syncNoticeToNewRegisterUser(String uid) {
QueryWrapper<AdminSysNotice> adminSysNoticeQueryWrapper = new QueryWrapper<>();
adminSysNoticeQueryWrapper
.eq("type", "All")
.le("gmt_create", new Date())
.eq("state", true);
List<AdminSysNotice> adminSysNotices = adminSysNoticeMapper.selectList(adminSysNoticeQueryWrapper);
if (adminSysNotices.size() == 0) {
return;
}
List<UserSysNotice> userSysNoticeList = new ArrayList<>();
for (AdminSysNotice adminSysNotice : adminSysNotices) {
UserSysNotice userSysNotice = new UserSysNotice();
userSysNotice.setType("Sys")
.setSysNoticeId(adminSysNotice.getId())
.setRecipientId(uid);
userSysNoticeList.add(userSysNotice);
}
userSysNoticeService.saveOrUpdateBatch(userSysNoticeList);
}
@Override
@Async
public void syncNoticeToNewRegisterBatchUser(List<String> uidList) {
QueryWrapper<AdminSysNotice> adminSysNoticeQueryWrapper = new QueryWrapper<>();
adminSysNoticeQueryWrapper
.eq("type", "All")
.le("gmt_create", new Date())
.eq("state", true);
List<AdminSysNotice> adminSysNotices = adminSysNoticeMapper.selectList(adminSysNoticeQueryWrapper);
if (adminSysNotices.size() == 0) {
return;
}
List<UserSysNotice> userSysNoticeList = new ArrayList<>();
for (String uid : uidList) {
for (AdminSysNotice adminSysNotice : adminSysNotices) {
UserSysNotice userSysNotice = new UserSysNotice();
userSysNotice.setType("Sys")
.setSysNoticeId(adminSysNotice.getId())
.setRecipientId(uid);
userSysNoticeList.add(userSysNotice);
}
}
userSysNoticeService.saveOrUpdateBatch(userSysNoticeList);
}
@Override
@Transactional
@Async
public void addSingleNoticeToUser(String adminId, String recipientId, String title, String content, String type) {
AdminSysNotice adminSysNotice = new AdminSysNotice();
adminSysNotice.setAdminId(adminId)
.setType("Single")
.setTitle(title)
.setContent(content)
.setState(true)
.setRecipientId(recipientId);
boolean isOk = adminSysNoticeMapper.insert(adminSysNotice) > 0;
if (isOk) {
UserSysNotice userSysNotice = new UserSysNotice();
userSysNotice.setRecipientId(recipientId)
.setSysNoticeId(adminSysNotice.getId())
.setType(type);
userSysNoticeService.save(userSysNotice);
}
}
}

View File

@ -4,17 +4,16 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import top.hcode.hoj.pojo.entity.Comment;
import org.springframework.scheduling.annotation.Async;
import top.hcode.hoj.pojo.entity.*;
import top.hcode.hoj.dao.CommentMapper;
import top.hcode.hoj.pojo.entity.Contest;
import top.hcode.hoj.pojo.entity.Reply;
import top.hcode.hoj.pojo.entity.UserInfo;
import top.hcode.hoj.pojo.vo.CommentsVo;
import top.hcode.hoj.service.CommentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import top.hcode.hoj.utils.Constants;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@ -41,6 +40,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
@Autowired
private ReplyServiceImpl replyService;
@Resource
private MsgRemindServiceImpl msgRemindService;
@Override
public IPage<CommentsVo> getCommentList(int limit, int currentPage, Long cid, Integer did, Boolean isRoot, String uid) {
//新建分页
@ -84,4 +86,36 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
replyQueryWrapper.orderByDesc("gmt_create");
return replyService.list(replyQueryWrapper);
}
@Async
public void updateCommentMsg(String recipientId, String senderId, String content, Integer discussionId) {
if (content.length() > 200) {
content = content.substring(0, 200) + "...";
}
MsgRemind msgRemind = new MsgRemind();
msgRemind.setAction("Discuss")
.setRecipientId(recipientId)
.setSenderId(senderId)
.setSourceContent(content)
.setSourceId(discussionId)
.setSourceType("Discussion")
.setUrl("/discussion-detail/" + discussionId);
msgRemindService.saveOrUpdate(msgRemind);
}
@Async
public void updateCommentLikeMsg(String recipientId, String senderId, Integer sourceId, String sourceType) {
MsgRemind msgRemind = new MsgRemind();
msgRemind.setAction("Like_Discuss")
.setRecipientId(recipientId)
.setSenderId(senderId)
.setSourceId(sourceId)
.setSourceType(sourceType)
.setUrl(sourceType.equals("Discussion") ? "/discussion-detail/" + sourceId : "/contest/" + sourceId + "/comment");
msgRemindService.saveOrUpdate(msgRemind);
}
}

View File

@ -1,8 +1,12 @@
package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import top.hcode.hoj.pojo.entity.ContestProblem;
import top.hcode.hoj.dao.ContestProblemMapper;
import top.hcode.hoj.pojo.entity.ContestRecord;
import top.hcode.hoj.pojo.entity.UserInfo;
import top.hcode.hoj.pojo.vo.ContestProblemVo;
import top.hcode.hoj.service.ContestProblemService;
@ -39,4 +43,14 @@ public class ContestProblemServiceImpl extends ServiceImpl<ContestProblemMapper,
return contestProblemMapper.getContestProblemList(cid, startTime, endTime, sealTime, isAdmin, superAdminUidList);
}
@Async
public void syncContestRecord(Long pid, Long cid, String displayId) {
UpdateWrapper<ContestRecord> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("pid", pid)
.eq("cid", cid)
.set("display_id", displayId);
contestRecordService.update(updateWrapper);
}
}

View File

@ -157,7 +157,7 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
public Page<OIContestRankVo> getOIContestRank(Long cid, String contestAuthor, Boolean isOpenSealRank, Date sealTime,
Date startTime, Date endTime, int currentPage, int limit) {
Date startTime, Date endTime, int currentPage, int limit) {
List<ContestRecord> oiContestRecord = contestRecordMapper.getOIContestRecord(cid, contestAuthor, isOpenSealRank, sealTime, startTime, endTime);
// 计算排名
@ -353,12 +353,18 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
oiContestRankVo = result.get(uidMapIndex.get(contestRecord.getUid())); // 根据记录的index进行获取
}
// 记录已经是每道題最新的提交了
oiContestRankVo.getSubmissionInfo().put(contestRecord.getDisplayId(), contestRecord.getScore());
// 记录总分
HashMap<String, Integer> submissionInfo = oiContestRankVo.getSubmissionInfo();
Integer score = submissionInfo.get(contestRecord.getDisplayId());
if (contestRecord.getScore() != null) { // 一般來说不可能出现status已经筛掉没有评分的提交记录
oiContestRankVo.setTotalScore(oiContestRankVo.getTotalScore() + contestRecord.getScore());
if (contestRecord.getScore() != null) {
if (score != null) { // 为了避免同个提交时间的重复计算
oiContestRankVo.setTotalScore(oiContestRankVo.getTotalScore() - score + contestRecord.getScore());
} else {
oiContestRankVo.setTotalScore(oiContestRankVo.getTotalScore() + contestRecord.getScore());
}
}
submissionInfo.put(contestRecord.getDisplayId(), contestRecord.getScore());
}

View File

@ -2,12 +2,16 @@ package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.hcode.hoj.dao.DiscussionMapper;
import top.hcode.hoj.pojo.entity.Discussion;
import top.hcode.hoj.pojo.entity.MsgRemind;
import top.hcode.hoj.pojo.vo.DiscussionVo;
import top.hcode.hoj.service.DiscussionService;
import javax.annotation.Resource;
/**
* @Author: Himit_ZH
* @Date: 2021/5/4 22:31
@ -23,4 +27,20 @@ public class DiscussionServiceImpl extends ServiceImpl<DiscussionMapper, Discuss
public DiscussionVo getDiscussion(Integer did, String uid) {
return discussionMapper.getDiscussion(did, uid);
}
@Resource
private MsgRemindServiceImpl msgRemindService;
@Async
public void updatePostLikeMsg(String recipientId, String senderId, Integer discussionId) {
MsgRemind msgRemind = new MsgRemind();
msgRemind.setAction("Like_Post")
.setRecipientId(recipientId)
.setSenderId(senderId)
.setSourceId(discussionId)
.setSourceType("Discussion")
.setUrl("/discussion-detail/" + discussionId);
msgRemindService.saveOrUpdate(msgRemind);
}
}

View File

@ -0,0 +1,197 @@
package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.hcode.hoj.dao.CommentMapper;
import top.hcode.hoj.dao.DiscussionMapper;
import top.hcode.hoj.dao.MsgRemindMapper;
import top.hcode.hoj.dao.ReplyMapper;
import top.hcode.hoj.pojo.entity.*;
import top.hcode.hoj.pojo.vo.UserMsgVo;
import top.hcode.hoj.pojo.vo.UserUnreadMsgCountVo;
import top.hcode.hoj.service.MsgRemindService;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:36
* @Description:
*/
@Service
public class MsgRemindServiceImpl extends ServiceImpl<MsgRemindMapper, MsgRemind> implements MsgRemindService {
@Resource
private MsgRemindMapper msgRemindMapper;
@Resource
private DiscussionMapper discussionMapper;
@Resource
private ContestServiceImpl contestService;
@Resource
private ApplicationContext applicationContext;
@Resource
private ReplyMapper replyMapper;
@Resource
private CommentMapper commentMapper;
@Resource
private UserSysNoticeServiceImpl userSysNoticeService;
@Override
public UserUnreadMsgCountVo getUserUnreadMsgCount(String uid) {
return msgRemindMapper.getUserUnreadMsgCount(uid);
}
@Override
public boolean cleanMsgByType(String type, Long id, String uid) {
switch (type) {
case "Like":
case "Discuss":
case "Reply":
UpdateWrapper<MsgRemind> updateWrapper1 = new UpdateWrapper<>();
updateWrapper1
.eq(id != null, "id", id)
.eq("recipient_id", uid);
return msgRemindMapper.delete(updateWrapper1) > 0;
case "Sys":
case "Mine":
UpdateWrapper<UserSysNotice> updateWrapper2 = new UpdateWrapper<>();
updateWrapper2
.eq(id != null, "id", id)
.eq("recipient_id", uid);
return userSysNoticeService.remove(updateWrapper2);
}
return false;
}
@Override
public IPage<UserMsgVo> getUserMsgList(String uid, String action, int limit, int currentPage) {
Page<UserMsgVo> page = new Page<>(currentPage, limit);
IPage<UserMsgVo> userMsgList = msgRemindMapper.getUserMsg(page, uid, action);
if (userMsgList.getTotal() > 0) {
switch (action) {
case "Discuss": // 评论我的
return getUserDiscussMsgList(userMsgList);
case "Reply": // 回复我的
return getUserReplyMsgList(userMsgList);
case "Like":
return getUserLikeMsgList(userMsgList);
default:
throw new RuntimeException("invalid action:" + action);
}
} else {
return userMsgList;
}
}
@Async
public void updateUserMsgRead(IPage<UserMsgVo> userMsgList) {
List<Long> idList = userMsgList.getRecords().stream()
.filter(userMsgVo -> !userMsgVo.getState())
.map(UserMsgVo::getId)
.collect(Collectors.toList());
if (idList.size() == 0) {
return;
}
UpdateWrapper<MsgRemind> updateWrapper = new UpdateWrapper<>();
updateWrapper.in("id", idList)
.set("state", true);
msgRemindMapper.update(null, updateWrapper);
}
public IPage<UserMsgVo> getUserDiscussMsgList(IPage<UserMsgVo> userMsgList) {
List<Integer> discussionIds = userMsgList.getRecords()
.stream()
.map(UserMsgVo::getSourceId)
.collect(Collectors.toList());
Collection<Discussion> discussions = discussionMapper.selectBatchIds(discussionIds);
for (Discussion discussion : discussions) {
for (UserMsgVo userMsgVo : userMsgList.getRecords()) {
if (Objects.equals(discussion.getId(), userMsgVo.getSourceId())) {
userMsgVo.setSourceTitle(discussion.getTitle());
break;
}
}
}
applicationContext.getBean(MsgRemindServiceImpl.class).updateUserMsgRead(userMsgList);
return userMsgList;
}
public IPage<UserMsgVo> getUserReplyMsgList(IPage<UserMsgVo> userMsgList) {
for (UserMsgVo userMsgVo : userMsgList.getRecords()) {
if ("Discussion".equals(userMsgVo.getSourceType())) {
Discussion discussion = discussionMapper.selectById(userMsgVo.getSourceId());
userMsgVo.setSourceTitle(discussion.getTitle());
} else if ("Contest".equals(userMsgVo.getSourceType())) {
Contest contest = contestService.getById(userMsgVo.getSourceId());
userMsgVo.setSourceTitle(contest.getTitle());
}
if ("Comment".equals(userMsgVo.getQuoteType())) {
Comment comment = commentMapper.selectById(userMsgVo.getQuoteId());
String content;
if (comment.getContent().length() < 100) {
content = comment.getFromName() + " : "
+ comment.getContent();
} else {
content = comment.getFromName() + " : "
+ comment.getContent().substring(0, 100) + "...";
}
userMsgVo.setQuoteContent(content);
} else if ("Reply".equals(userMsgVo.getQuoteType())) {
Reply reply = replyMapper.selectById(userMsgVo.getQuoteId());
String content;
if (reply.getContent().length() < 100) {
content = reply.getFromName() + " : @" + reply.getToName() + ""
+ reply.getContent();
} else {
content = reply.getFromName() + " : @" + reply.getToName() + ""
+ reply.getContent().substring(0, 100) + "...";
}
userMsgVo.setQuoteContent(content);
}
}
applicationContext.getBean(MsgRemindServiceImpl.class).updateUserMsgRead(userMsgList);
return userMsgList;
}
public IPage<UserMsgVo> getUserLikeMsgList(IPage<UserMsgVo> userMsgList) {
for (UserMsgVo userMsgVo : userMsgList.getRecords()) {
if ("Discussion".equals(userMsgVo.getSourceType())) {
Discussion discussion = discussionMapper.selectById(userMsgVo.getSourceId());
userMsgVo.setSourceTitle(discussion.getTitle());
} else if ("Contest".equals(userMsgVo.getSourceType())) {
Contest contest = contestService.getById(userMsgVo.getSourceId());
userMsgVo.setSourceTitle(contest.getTitle());
}
}
applicationContext.getBean(MsgRemindServiceImpl.class).updateUserMsgRead(userMsgList);
return userMsgList;
}
}

View File

@ -33,6 +33,8 @@ import top.hcode.hoj.utils.Constants;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@ -680,10 +682,15 @@ public class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> impl
return importProblemVo;
}
// 去除末尾的空白符
// 去除每行末尾的空白符
public static String rtrim(String value) {
if (value == null) return null;
return value.replaceAll("\\s+$", "");
StringBuilder sb = new StringBuilder();
String[] strArr = value.split("\n");
for (String str : strArr) {
sb.append(str.replaceAll("\\s+$", "")).append("\n");
}
return sb.toString().replaceAll("\\s+$", "");
}
}

View File

@ -1,11 +1,15 @@
package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.hcode.hoj.dao.ReplyMapper;
import top.hcode.hoj.pojo.entity.MsgRemind;
import top.hcode.hoj.pojo.entity.Reply;
import top.hcode.hoj.service.ReplyService;
import javax.annotation.Resource;
/**
* @Author: Himit_ZH
* @Date: 2021/5/5 22:09
@ -13,4 +17,28 @@ import top.hcode.hoj.service.ReplyService;
*/
@Service
public class ReplyServiceImpl extends ServiceImpl<ReplyMapper, Reply> implements ReplyService {
@Resource
private MsgRemindServiceImpl msgRemindService;
@Async
public void updateReplyMsg(Integer sourceId, String sourceType, String content, Integer quoteId, String quoteType,
String recipientId,String senderId) {
if (content.length() > 200) {
content = content.substring(0, 200) + "...";
}
MsgRemind msgRemind = new MsgRemind();
msgRemind.setAction("Reply")
.setSourceId(sourceId)
.setSourceType(sourceType)
.setSourceContent(content)
.setQuoteId(quoteId)
.setQuoteType(quoteType)
.setUrl(sourceType.equals("Discussion") ? "/discussion-detail/" + sourceId : "/contest/" + sourceId + "/comment")
.setRecipientId(recipientId)
.setSenderId(senderId);
msgRemindService.saveOrUpdate(msgRemind);
}
}

View File

@ -1,11 +1,26 @@
package top.hcode.hoj.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import top.hcode.hoj.dao.SessionMapper;
import top.hcode.hoj.pojo.entity.AdminSysNotice;
import top.hcode.hoj.pojo.entity.Session;
import top.hcode.hoj.pojo.entity.UserSysNotice;
import top.hcode.hoj.service.AdminSysNoticeService;
import top.hcode.hoj.service.SessionService;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2020/12/3 22:46
@ -13,4 +28,86 @@ import top.hcode.hoj.service.SessionService;
*/
@Service
public class SessionServiceImpl extends ServiceImpl<SessionMapper, Session> implements SessionService {
@Resource
private SessionMapper sessionMapper;
@Resource
private AdminSysNoticeServiceImpl adminSysNoticeService;
@Resource
private UserSysNoticeServiceImpl userSysNoticeService;
@Override
@Async
@Transactional
public void checkRemoteLogin(String uid) {
QueryWrapper<Session> sessionQueryWrapper = new QueryWrapper<>();
sessionQueryWrapper.eq("uid", uid)
.orderByDesc("gmt_create")
.last("limit 2");
List<Session> sessionList = sessionMapper.selectList(sessionQueryWrapper);
if (sessionList.size() < 2) {
return;
}
Session nowSession = sessionList.get(0);
Session lastSession = sessionList.get(1);
// 如果两次登录的ip不相同需要发通知给用户
if (!nowSession.getIp().equals(lastSession.getIp())) {
AdminSysNotice adminSysNotice = new AdminSysNotice();
adminSysNotice
.setType("Single")
.setContent(getRemoteLoginContent(nowSession.getIp(), nowSession.getGmtCreate()))
.setTitle("账号异地登录通知(Account Remote Login Notice)")
.setAdminId("1")
.setState(false)
.setRecipientId(uid);
boolean isSaveOk = adminSysNoticeService.save(adminSysNotice);
if (isSaveOk) {
UserSysNotice userSysNotice = new UserSysNotice();
userSysNotice.setType("Sys")
.setSysNoticeId(adminSysNotice.getId())
.setRecipientId(uid)
.setState(false);
boolean isOk = userSysNoticeService.save(userSysNotice);
if (isOk) {
adminSysNotice.setState(true);
adminSysNoticeService.saveOrUpdate(adminSysNotice);
}
}
}
}
private String getRemoteLoginContent(String newIp, Date loginDate) {
String dateStr = DateUtil.format(loginDate, "yyyy-MM-dd HH:mm:ss");
StringBuilder sb = new StringBuilder();
sb.append("亲爱的用户,您好!您的账号于").append(dateStr);
String addr = null;
try {
String res = HttpUtil.get("https://whois.pconline.com.cn/ipJson.jsp?ip=" + newIp + "&json=true");
JSONObject resJson = JSONUtil.parseObj(res);
addr = resJson.getStr("addr");
} catch (Exception ignored) {
}
if (!StringUtils.isEmpty(addr)) {
sb.append("在【")
.append(addr)
.append("");
}
sb.append("登录登录IP为")
.append(newIp)
.append("】,若非本人操作,请立即修改密码。")
.append("\n\n")
.append("Hello! Dear user, Your account was logged in in");
if (!StringUtils.isEmpty(addr)) {
sb.append("")
.append(addr)
.append("】 on ")
.append(dateStr)
.append(". If you do not operate by yourself, please change your password immediately.");
}
return sb.toString();
}
}

View File

@ -22,12 +22,14 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import top.hcode.hoj.shiro.AccountProfile;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* <p>
* 服务实现类
* 服务实现类
* </p>
*
* @author Himit_ZH
@ -42,32 +44,32 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i
RedisSessionDAO redisSessionDAO;
@Override
public IPage<UserRolesVo> getUserList(int limit, int currentPage, String keyword,Boolean onlyAdmin) {
public IPage<UserRolesVo> getUserList(int limit, int currentPage, String keyword, Boolean onlyAdmin) {
//新建分页
Page<UserRolesVo> page = new Page<>(currentPage, limit);
if (onlyAdmin){
return userRoleMapper.getAdminUserList(page,limit, currentPage,keyword);
}else {
if (onlyAdmin) {
return userRoleMapper.getAdminUserList(page, limit, currentPage, keyword);
} else {
return userRoleMapper.getUserList(page, limit, currentPage, keyword);
}
}
/**
* @MethodName deleteCache
* @param uid 当前需要操作的用户id
* @param uid 当前需要操作的用户id
* @param isRemoveSession 如果为true则会强行删除该用户session必须重新登陆false的话 在访问受限接口时会重新授权
* @MethodName deleteCache
* @Description TODO
* @Return
* @Since 2021/6/12
*/
@Override
public void deleteCache(String uid, boolean isRemoveSession){
public void deleteCache(String uid, boolean isRemoveSession) {
//从缓存中获取Session
Session session = null;
Collection<Session> sessions = redisSessionDAO.getActiveSessions();
AccountProfile accountProfile;
Object attribute = null;
for(Session sessionInfo : sessions){
for (Session sessionInfo : sessions) {
//遍历Session,找到该用户名称对应的Session
attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
@ -78,11 +80,11 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i
continue;
}
if (Objects.equals(accountProfile.getUid(), uid)) {
session=sessionInfo;
session = sessionInfo;
break;
}
}
if (session == null||attribute == null) {
if (session == null || attribute == null) {
return;
}
//删除session 会强制退出主要是在禁用用户或角色时强制用户退出的
@ -96,4 +98,29 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i
((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
}
private final static List<String> ChineseRole = Arrays.asList("超级管理员", "普通管理员",
"普通用户(默认)", "普通用户(禁止提交)", "普通用户(禁止发讨论)", "普通用户(禁言)", "普通用户(禁止提交&禁止发讨论)",
"用户(禁止提交&禁言)", "题目管理员");
private final static List<String> EnglishRole = Arrays.asList("Super Administrator", "General Administrator",
"Normal User(Default)", "Normal User(No Submission)", "Normal User(No Discussion)", "Normal User(Forbidden Words)",
"Normal User(No Submission & No Discussion)",
"Normal User(No Submission & Forbidden Words)", "Problem Administrator");
@Override
public String getAuthChangeContent(int oldType, int newType) {
String stringBuffer = "您好,您的权限产生了变更,由【" +
ChineseRole.get(oldType - 1000) +
"】变更为【" +
ChineseRole.get(newType - 1000) +
"】。部分权限可能与之前有所不同,请您注意!" +
"\n\n" +
"Hello, your permission has been changed from 【" +
EnglishRole.get(oldType - 1000) +
"】 to 【" +
EnglishRole.get(newType - 1000) +
"】. Some permissions may be different from before. Please note!";
return stringBuffer.toString();
}
}

View File

@ -0,0 +1,65 @@
package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.hcode.hoj.dao.UserSysNoticeMapper;
import top.hcode.hoj.pojo.entity.MsgRemind;
import top.hcode.hoj.pojo.entity.UserSysNotice;
import top.hcode.hoj.pojo.vo.SysMsgVo;
import top.hcode.hoj.pojo.vo.UserMsgVo;
import top.hcode.hoj.service.UserSysNoticeService;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:35
* @Description:
*/
@Service
public class UserSysNoticeServiceImpl extends ServiceImpl<UserSysNoticeMapper, UserSysNotice> implements UserSysNoticeService {
@Resource
private UserSysNoticeMapper userSysNoticeMapper;
@Resource
private ApplicationContext applicationContext;
@Override
public IPage<SysMsgVo> getSysNotice(int limit, int currentPage, String uid) {
Page<SysMsgVo> page = new Page<>(currentPage, limit);
IPage<SysMsgVo> sysNotice = userSysNoticeMapper.getSysOrMineNotice(page, uid, "Sys");
applicationContext.getBean(UserSysNoticeServiceImpl.class).updateSysOrMineMsgRead(sysNotice);
return sysNotice;
}
@Override
public IPage<SysMsgVo> getMineNotice(int limit, int currentPage, String uid) {
Page<SysMsgVo> page = new Page<>(currentPage, limit);
IPage<SysMsgVo> mineNotice = userSysNoticeMapper.getSysOrMineNotice(page, uid, "Mine");
applicationContext.getBean(UserSysNoticeServiceImpl.class).updateSysOrMineMsgRead(mineNotice);
return mineNotice;
}
@Async
public void updateSysOrMineMsgRead(IPage<SysMsgVo> userMsgList) {
List<Long> idList = userMsgList.getRecords().stream()
.filter(userMsgVo -> !userMsgVo.getState())
.map(SysMsgVo::getId)
.collect(Collectors.toList());
if (idList.size() == 0) {
return;
}
UpdateWrapper<UserSysNotice> updateWrapper = new UpdateWrapper<>();
updateWrapper.in("id", idList)
.set("state", true);
userSysNoticeMapper.update(null, updateWrapper);
}
}

View File

@ -9,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import top.hcode.hoj.common.CommonResult;
@ -73,7 +74,7 @@ public class JudgeController {
HashMap<String, Object> res = new HashMap<>();
res.put("version", "1.5.0");
res.put("version", "1.6.0");
res.put("currentTime", new Date());
res.put("judgeServerName", name);
res.put("cpu", Runtime.getRuntime().availableProcessors());

View File

@ -82,9 +82,9 @@ public class JudgeRun {
List<FutureTask<JSONObject>> futureTasks = new ArrayList<>();
JSONArray testcaseList = (JSONArray) testCasesInfo.get("testCases");
Boolean isSpj = testCasesInfo.getBool("isSpj");
// 默认给1.1倍题目限制时间用来测评
Double time = maxTime * 1.1;
final Long testTime = time.longValue();
// 默认给题目限制时间+200ms用来测评
final Long testTime = maxTime + 200;
// 用户输出的文件夹
String runDir = Constants.JudgeDir.RUN_WORKPLACE_DIR.getContent() + File.separator + submitId;
@ -115,7 +115,8 @@ public class JudgeRun {
testCaseId,
runDir,
testCaseInputPath,
testTime,// 默认给1.1倍题目限制时间用来测评
testTime,// 默认给题目限制时间+200ms用来测评
maxTime,
maxMemory,
maxStack,
maxOutputSize,
@ -138,7 +139,8 @@ public class JudgeRun {
runDir,
testCaseInputPath,
testCaseOutputPath,
testTime,// 默认给1.1倍题目限制时间用来测评
testTime,// 默认给题目限制时间+200ms用来测评
maxTime,
maxMemory,
maxOutputSize,
maxStack,
@ -184,6 +186,7 @@ public class JudgeRun {
String runDir,
String testCaseInputFilePath,
String testCaseOutputFilePath,
Long testTime,
Long maxTime,
Long maxMemory,
Long maxOutputSize,
@ -195,7 +198,7 @@ public class JudgeRun {
parseRunCommand(runConfig.getCommand(), runConfig, null, null, null),
runConfig.getEnvs(),
testCaseInputFilePath,
maxTime,
testTime,
maxOutputSize,
maxStack,
runConfig.getExeName(),
@ -388,6 +391,7 @@ public class JudgeRun {
Integer testCaseId,
String runDir,
String testCasePath,
Long testTime,
Long maxTime,
Long maxMemory,
Integer maxStack,
@ -399,7 +403,7 @@ public class JudgeRun {
JSONArray judgeResultList = SandboxRun.testCase(parseRunCommand(runConfig.getCommand(), runConfig, null, null, null),
runConfig.getEnvs(),
testCasePath,
maxTime,
testTime,
maxOutputSize,
maxStack,
runConfig.getExeName(),
@ -522,10 +526,15 @@ public class JudgeRun {
}
}
// 去除末尾空白符
// 去除末尾空白符
public static String rtrim(String value) {
if (value == null) return null;
return value.replaceAll("\\s+$", "");
StringBuilder sb = new StringBuilder();
String[] strArr = value.split("\n");
for (String str : strArr) {
sb.append(str.replaceAll("\\s+$", "")).append("\n");
}
return sb.toString().replaceAll("\\s+$", "");
}
/*

View File

@ -88,7 +88,7 @@ public class ProblemTestCaseUtils {
testCaseList.add(jsonObject);
}
result.set("testCases",testCaseList);
result.set("testCases", testCaseList);
FileWriter infoFile = new FileWriter(testCasesDir + File.separator + "info", CharsetUtil.UTF_8);
// 写入记录文件
@ -194,9 +194,14 @@ public class ProblemTestCaseUtils {
}
}
// 去除末尾的空白符
// 去除每行末尾的空白符
public static String rtrim(String value) {
if (value == null) return null;
return value.replaceAll("\\s+$", "");
StringBuilder sb = new StringBuilder();
String[] strArr = value.split("\n");
for (String str : strArr) {
sb.append(str.replaceAll("\\s+$", "")).append("\n");
}
return sb.toString().replaceAll("\\s+$", "");
}
}

View File

@ -70,7 +70,7 @@ public class SandboxRun {
private static final int maxProcessNumber = 128;
private static final int TIME_LIMIT_MS = 8000;
private static final int TIME_LIMIT_MS = 16000;
private static final int MEMORY_LIMIT_MB = 512;

View File

@ -18,7 +18,7 @@ import java.util.regex.Pattern;
@Slf4j(topic = "hoj")
public class HduJudge implements RemoteJudgeStrategy {
public static final String HOST = "https://acm.hdu.edu.cn";
public static final String HOST = "https://acm.dingbacode.com";
public static final String LOGIN_URL = "/userloginex.php?action=login";
public static final String SUBMIT_URL = "/submit.php?action=submit";
public static final String STATUS_URL = "/status.php?user=%s&pid=%s";

View File

@ -0,0 +1,54 @@
package top.hcode.hoj.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:11
* @Description:
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="AdminSysNotice", description="")
public class AdminSysNotice {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "通知标题")
private String title;
@ApiModelProperty(value = "通知内容")
private String content;
@ApiModelProperty(value = "发给哪些用户类型,例如全部用户All指定单个用户Single管理员Admin")
private String type;
@ApiModelProperty(value = "是否已被拉取过,如果已经拉取过,就无需再次拉取")
private Boolean state;
@ApiModelProperty(value = "接受通知的用户的id如果type为single那么recipient 为该用户的id;否则recipient为null")
private String recipientId;
@ApiModelProperty(value = "发布通知的管理员id")
private String adminId;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
}

View File

@ -0,0 +1,68 @@
package top.hcode.hoj.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:21
* @Description:
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="MsgRemind", description="")
public class MsgRemind {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "动作类型如点赞讨论帖Like_Post、点赞评论Like_Discuss、评论Discuss、回复Reply等")
private String action;
@ApiModelProperty(value = "消息来源id讨论id或比赛id")
private Integer sourceId;
@ApiModelProperty(value = "事件源类型:'Discussion'、'Contest'等")
private String sourceType;
@ApiModelProperty(value = "事件源的内容,比如回复的内容,回复的评论等等,不超过250字符超过使用...")
private String sourceContent;
@ApiModelProperty(value = "事件引用上一级评论或回复id")
private Integer quoteId;
@ApiModelProperty(value = "事件引用上一级的类型Comment、Reply")
private String quoteType;
@ApiModelProperty(value = "事件所发生的地点链接 url")
private String url;
@ApiModelProperty(value = "接受通知的用户的id")
private String recipientId;
@ApiModelProperty(value = "动作执行者的id")
private String senderId;
@ApiModelProperty(value = "是否已读")
private Boolean state;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
}

View File

@ -92,7 +92,7 @@ public class Problem implements Serializable {
@TableField(value="spj_language",updateStrategy = FieldStrategy.IGNORED)
private String spjLanguage;
@ApiModelProperty(value = "是否默认去除用户代码的文末空格")
@ApiModelProperty(value = "是否默认去除用户代码的每行末尾空白符")
private Boolean isRemoveEndBlank;
@ApiModelProperty(value = "是否默认开启该题目的测试样例结果查看")

View File

@ -0,0 +1,48 @@
package top.hcode.hoj.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* @Author: Himit_ZH
* @Date: 2021/10/1 20:18
* @Description:
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UserSysNotice", description="")
public class UserSysNotice {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "系统通知的id")
private Long sysNoticeId;
@ApiModelProperty(value = "接受通知的用户的id")
private String recipientId;
@ApiModelProperty(value = "消息类型系统通知Sys、我的信息Mine")
private String type;
@ApiModelProperty(value = "是否已读")
private Boolean state;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
}

View File

@ -2565,7 +2565,7 @@
},
"async-validator": {
"version": "1.8.5",
"resolved": "https://registry.npm.taobao.org/async-validator/download/async-validator-1.8.5.tgz?cache=0&sync_timestamp=1596623572478&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fasync-validator%2Fdownload%2Fasync-validator-1.8.5.tgz",
"resolved": "https://registry.nlark.com/async-validator/download/async-validator-1.8.5.tgz",
"integrity": "sha1-3D4I7B/Q3dtn5ghC8CwM0c7G1/A=",
"requires": {
"babel-runtime": "6.x"
@ -2647,7 +2647,7 @@
},
"babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npm.taobao.org/babel-runtime/download/babel-runtime-6.26.0.tgz",
"resolved": "https://registry.npm.taobao.org/babel-runtime/download/babel-runtime-6.26.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbabel-runtime%2Fdownload%2Fbabel-runtime-6.26.0.tgz",
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"requires": {
"core-js": "^2.4.0",
@ -2655,13 +2655,13 @@
},
"dependencies": {
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npm.taobao.org/core-js/download/core-js-2.6.11.tgz?cache=0&sync_timestamp=1586450269267&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcore-js%2Fdownload%2Fcore-js-2.6.11.tgz",
"integrity": "sha1-OIMUafmSK97Y7iHJ3EaYXgOZMIw="
"version": "2.6.12",
"resolved": "https://registry.npmmirror.com/core-js/download/core-js-2.6.12.tgz",
"integrity": "sha1-2TM9+nsGXjR8xWgiGdb2kIWcwuw="
},
"regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.11.1.tgz?cache=0&sync_timestamp=1595456311465&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fregenerator-runtime%2Fdownload%2Fregenerator-runtime-0.11.1.tgz",
"resolved": "https://registry.nlark.com/regenerator-runtime/download/regenerator-runtime-0.11.1.tgz?cache=0&sync_timestamp=1626993702812&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fregenerator-runtime%2Fdownload%2Fregenerator-runtime-0.11.1.tgz",
"integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk="
}
}
@ -4902,9 +4902,9 @@
"dev": true
},
"element-ui": {
"version": "2.14.0",
"resolved": "https://registry.npm.taobao.org/element-ui/download/element-ui-2.14.0.tgz?cache=0&sync_timestamp=1603958155882&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Felement-ui%2Fdownload%2Felement-ui-2.14.0.tgz",
"integrity": "sha1-3P8nGPtIVULqW1ofXA2vERNHeyQ=",
"version": "2.15.6",
"resolved": "https://registry.nlark.com/element-ui/download/element-ui-2.15.6.tgz",
"integrity": "sha1-yWCa3TWvWmhqS3aF3B11fHXgHfM=",
"requires": {
"async-validator": "~1.8.1",
"babel-helper-vue-jsx-merge-props": "^2.0.0",

View File

@ -13,7 +13,7 @@
"compression-webpack-plugin": "^5.0.1",
"core-js": "^3.6.5",
"echarts": "^4.9.0",
"element-ui": "^2.14.0",
"element-ui": "^2.15.3",
"font-awesome": "^4.7.0",
"highlight.js": "^10.3.2",
"jquery": "^3.5.1",

View File

@ -515,6 +515,15 @@ footer h1 {
display: none !important;
}
}
.el-empty {
max-width: 256px;
margin: 0 auto;
}
.el-empty__description {
text-align: center;
color: #3498db;
font-size: 13px;
}
</style>
<style>
.markdown-body pre {

View File

@ -556,11 +556,13 @@ const ojApi = {
})
},
toLikeComment(cid,toLike){
toLikeComment(cid,toLike,sourceId,sourceType){
return ajax("/api/comment-like",'get',{
params:{
cid,
toLike
toLike,
sourceId,
sourceType
}
})
},
@ -584,6 +586,71 @@ const ojApi = {
cid
}
})
},
// 站内消息
getUnreadMsgCount(){
return ajax("/api/msg/unread",'get')
},
getMsgList(routerName,searchParams){
let params ={};
Object.keys(searchParams).forEach((element) => {
if (searchParams[element]!==''&&searchParams[element]!==null&&searchParams[element]!==undefined) {
params[element] = searchParams[element]
}
})
switch(routerName){
case "DiscussMsg":
return ajax("/api/msg/comment",'get',{
params
});
case "ReplyMsg":
return ajax("/api/msg/reply",'get',{
params
});
case "LikeMsg":
return ajax("/api/msg/like",'get',{
params
});
case "SysMsg":
return ajax("/api/msg/sys",'get',{
params
});
case "MineMsg":
return ajax("/api/msg/mine",'get',{
params
});
}
},
cleanMsg(routerName,id){
let params ={};
if(id){
params.id=id;
}
switch(routerName){
case "DiscussMsg":
params.type = 'Discuss';
break;
case "ReplyMsg":
params.type = 'Reply';
break;
case "LikeMsg":
params.type = 'Like';
break;
case "SysMsg":
params.type = 'Sys';
break;
case "MineMsg":
params.type = 'Mine';
break;
}
return ajax("/api/msg/clean",'delete',{
params
});
}
}

View File

@ -165,6 +165,7 @@
id="commentbox"
v-for="(item, commentIndex) in comments"
:key="commentIndex"
v-loading="loading"
>
<div class="info">
<span
@ -322,7 +323,7 @@
</div>
</div>
<div class="view-more item" v-if="item.totalReplyNum > 3">
{{ $t('m.Reply_Total') }}<b>{{ item.totalReplyNum }}</b
{{ $t('m.Reply_Total') }}<b> {{ item.totalReplyNum }} </b
>{{ $t('m.Replies') }},
<a
class="btn-more"
@ -541,6 +542,9 @@ export default {
toName: '',
toAvatar: '',
},
replyQuoteId: null,
replyQuoteType: 'Comment',
loading: false,
};
},
@ -568,20 +572,27 @@ export default {
this.replyPlaceholder = this.$i18n.t(
'm.Come_and_write_down_your_comments'
);
api.getCommentList(queryParams).then((res) => {
let moreCommentList = res.data.data.commentList.records;
for (let i = 0; i < moreCommentList.length; i++) {
this.totalComment += 1 + moreCommentList[i].totalReplyNum;
this.loading = true;
api.getCommentList(queryParams).then(
(res) => {
let moreCommentList = res.data.data.commentList.records;
for (let i = 0; i < moreCommentList.length; i++) {
this.totalComment += 1 + moreCommentList[i].totalReplyNum;
}
this.comments = this.comments.concat(moreCommentList);
this.total = res.data.data.commentList.total;
this.commentLikeMap = res.data.data.commentLikeMap;
if (this.comments.length > 0) {
this.$nextTick((_) => {
addCodeBtn();
});
}
this.loading = false;
},
(err) => {
this.loading = false;
}
this.comments = this.comments.concat(moreCommentList);
this.total = res.data.data.commentList.total;
this.commentLikeMap = res.data.data.commentLikeMap;
if (this.comments.length > 0) {
this.$nextTick((_) => {
addCodeBtn();
});
}
});
);
},
/**
@ -590,7 +601,16 @@ export default {
likeClick(item) {
//
let toLike = this.commentLikeMap[item.id] != true;
api.toLikeComment(item.id, toLike).then((res) => {
let sourceId = this.did;
let sourceType = 'Discussion';
if (this.cid != null) {
sourceId = this.cid;
sourceType = 'Contest';
}
api.toLikeComment(item.id, toLike, sourceId, sourceType).then((res) => {
if (toLike) {
this.commentLikeMap[item.id] = true;
item.likeNum++;
@ -648,6 +668,8 @@ export default {
let replyData = {
reply: this.replyObj,
did: this.did,
quoteId: this.replyQuoteId,
quoteType: this.replyQuoteType,
};
api.addReply(replyData).then((res) => {
for (let i = 0; i < this.comments.length; i++) {
@ -690,6 +712,8 @@ export default {
this.replyObj.toUid = reply.fromUid;
this.replyObj.toName = reply.fromName;
this.replyObj.toAvatar = reply.fromAvatar;
this.replyQuoteId = reply.id;
this.replyQuoteType = 'Reply';
} else {
this.replyPlaceholder = this.$i18n.t(
'm.Come_and_write_down_your_comments'
@ -698,6 +722,8 @@ export default {
this.replyObj.toUid = item.fromUid;
this.replyObj.toName = item.fromName;
this.replyObj.toAvatar = item.fromAvatar;
this.replyQuoteId = item.id;
this.replyQuoteType = 'Comment';
}
this.showItemId = item.id;
},
@ -965,6 +991,8 @@ export default {
display: flex;
flex-wrap: wrap;
padding: 5px;
height: 200px;
overflow-y: scroll;
}
.emotionItem {
width: 10%;

View File

@ -29,7 +29,7 @@
v-if="!announcements.length"
key="no-announcement"
>
<p>{{ $t('m.No_Announcements') }}</p>
<el-empty :description="$t('m.No_Announcements')"></el-empty>
</div>
<template v-if="listVisible">
<ul class="announcements-container" key="list">

View File

@ -111,6 +111,62 @@
:src="avatar"
class="drop-avatar"
></avatar>
<el-dropdown
class="drop-msg"
@command="handleRoute"
placement="bottom"
>
<span class="el-dropdown-link">
<i class="el-icon-message-solid"></i>
<svg
v-if="
unreadMessage.comment > 0 ||
unreadMessage.reply > 0 ||
unreadMessage.like > 0 ||
unreadMessage.sys > 0 ||
unreadMessage.mine > 0
"
width="10"
height="10"
style="vertical-align: top;margin-left: -11px;margin-top: 3px;"
>
<circle cx="5" cy="5" r="5" style="fill: red;"></circle>
</svg>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="/message/discuss">
<span>{{ $t('m.DiscussMsg') }}</span>
<span class="drop-msg-count" v-if="unreadMessage.comment > 0">
<MsgSvg :total="unreadMessage.comment"></MsgSvg>
</span>
</el-dropdown-item>
<el-dropdown-item command="/message/reply">
<span>{{ $t('m.ReplyMsg') }}</span>
<span class="drop-msg-count" v-if="unreadMessage.reply > 0">
<MsgSvg :total="unreadMessage.reply"></MsgSvg>
</span>
</el-dropdown-item>
<el-dropdown-item command="/message/like">
<span>{{ $t('m.LikeMsg') }}</span>
<span class="drop-msg-count" v-if="unreadMessage.like > 0">
<MsgSvg :total="unreadMessage.like"></MsgSvg>
</span>
</el-dropdown-item>
<el-dropdown-item command="/message/sys">
<span>{{ $t('m.SysMsg') }}</span>
<span class="drop-msg-count" v-if="unreadMessage.sys > 0">
<MsgSvg :total="unreadMessage.sys"></MsgSvg>
</span>
</el-dropdown-item>
<el-dropdown-item command="/message/mine">
<span>{{ $t('m.MineMsg') }}</span>
<span class="drop-msg-count" v-if="unreadMessage.mine > 0">
<MsgSvg :total="unreadMessage.mine"></MsgSvg>
</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-menu>
</div>
@ -138,13 +194,98 @@
>{{ $t('m.NavBar_Register') }}</mu-button
>
<mu-menu slot="right" v-show="isAuthenticated" :open.sync="openmsgmenu">
<mu-button flat>
<mu-icon value=":el-icon-message-solid" size="24"></mu-icon>
<svg
v-if="
unreadMessage.comment > 0 ||
unreadMessage.reply > 0 ||
unreadMessage.like > 0 ||
unreadMessage.sys > 0 ||
unreadMessage.mine > 0
"
width="10"
height="10"
style="margin-left: -11px;margin-top: -13px;"
>
<circle cx="5" cy="5" r="5" style="fill: red;"></circle>
</svg>
</mu-button>
<mu-list slot="content" @change="handleCommand">
<mu-list-item button value="/message/discuss">
<mu-list-item-content>
<mu-list-item-title>
{{ $t('m.DiscussMsg') }}
<span class="drop-msg-count" v-if="unreadMessage.comment > 0">
<MsgSvg :total="unreadMessage.comment"></MsgSvg>
</span>
</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
<mu-divider></mu-divider>
<mu-list-item button value="/message/reply">
<mu-list-item-content>
<mu-list-item-title>
{{ $t('m.ReplyMsg') }}
<span class="drop-msg-count" v-if="unreadMessage.reply > 0">
<MsgSvg :total="unreadMessage.reply"></MsgSvg>
</span>
</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
<mu-divider></mu-divider>
<mu-list-item button value="/message/like">
<mu-list-item-content>
<mu-list-item-title>
{{ $t('m.LikeMsg') }}
<span class="drop-msg-count" v-if="unreadMessage.like > 0">
<MsgSvg :total="unreadMessage.like"></MsgSvg>
</span>
</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
<mu-divider></mu-divider>
<mu-list-item button value="/message/sys">
<mu-list-item-content>
<mu-list-item-title>
{{ $t('m.SysMsg') }}
<span class="drop-msg-count" v-if="unreadMessage.sys > 0">
<MsgSvg :total="unreadMessage.sys"></MsgSvg>
</span>
</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
<mu-divider></mu-divider>
<mu-list-item button value="/message/mine">
<mu-list-item-content>
<mu-list-item-title>
{{ $t('m.MineMsg') }}
<span class="drop-msg-count" v-if="unreadMessage.mine > 0">
<MsgSvg :total="unreadMessage.mine"></MsgSvg>
</span>
</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
</mu-list>
</mu-menu>
<mu-menu
slot="right"
v-show="isAuthenticated"
:open.sync="openusermenu"
>
<mu-button flat>
{{ userInfo.username }}<i class="el-icon-caret-bottom"></i>
<avatar
:username="userInfo.username"
:inline="true"
:size="30"
color="#FFF"
:src="userInfo.avatar"
:title="userInfo.username"
></avatar>
<i class="el-icon-caret-bottom"></i>
</mu-button>
<mu-list slot="content" @change="handleCommand">
<mu-list-item button value="/user-home">
@ -154,7 +295,7 @@
}}</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
<mu-divider></mu-divider>
<mu-list-item button value="/status?onlyMine=true">
<mu-list-item-content>
<mu-list-item-title>{{
@ -162,6 +303,7 @@
}}</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
<mu-divider></mu-divider>
<mu-list-item button value="/setting">
<mu-list-item-content>
<mu-list-item-title>{{
@ -169,7 +311,7 @@
}}</mu-list-item-title>
</mu-list-item-content>
</mu-list-item>
<mu-divider></mu-divider>
<mu-list-item button value="/admin" v-show="isAdminRole">
<mu-list-item-content>
<mu-list-item-title>{{
@ -367,21 +509,32 @@
import Login from '@/components/oj/common/Login';
import Register from '@/components/oj/common/Register';
import ResetPwd from '@/components/oj/common/ResetPassword';
import MsgSvg from '@/components/oj/msg/msgSvg';
import { mapGetters, mapActions } from 'vuex';
import Avatar from 'vue-avatar';
import api from '@/common/api';
export default {
components: {
Login,
Register,
ResetPwd,
Avatar,
MsgSvg,
},
mounted() {
window.onresize = () => {
this.page_width();
};
this.page_width();
if (this.isAuthenticated) {
this.getUnreadMsgCount();
this.msgTimer = setInterval(() => {
this.getUnreadMsgCount();
}, 120 * 1000);
}
},
beforeDestroy() {
clearInterval(this.msgTimer);
},
data() {
return {
@ -389,8 +542,10 @@ export default {
mobileNar: false,
opendrawer: false,
openusermenu: false,
openmsgmenu: false,
openSideMenu: '',
imgUrl: require('@/assets/logo.png'),
avatarStyle:
'display: inline-flex;width: 30px;height: 30px;border-radius: 50%;align-items: center;justify-content: center;text-align: center;user-select: none;',
};
@ -422,12 +577,18 @@ export default {
handleCommand(route) {
//
this.openusermenu = false;
this.openmsgmenu = false;
if (route && route.split('/')[1] != 'admin') {
this.$router.push(route);
} else {
window.open('/admin/');
}
},
getUnreadMsgCount() {
api.getUnreadMsgCount().then((res) => {
this.$store.dispatch('updateUnreadMessageCount', res.data.data);
});
},
},
computed: {
...mapGetters([
@ -437,6 +598,7 @@ export default {
'isAdminRole',
'token',
'websiteConfig',
'unreadMessage',
]),
avatar() {
return this.$store.getters.userInfo.avatar;
@ -535,6 +697,16 @@ export default {
position: relative;
margin-top: 16px;
}
.drop-msg {
float: right;
font-size: 25px;
margin-right: 10px;
position: relative;
margin-top: 13px;
}
.drop-msg-count {
margin-left: 2px;
}
.btn-menu {
font-size: 16px;
float: right;

View File

@ -11,6 +11,7 @@
:layout="layout"
:page-sizes="[10, 15, 30, 50, 100]"
:current-page="current"
:hide-on-single-page="total == 0"
></el-pagination>
</div>
</template>

View File

@ -0,0 +1,23 @@
<template>
<svg width="20" height="20">
<circle cx="10" cy="10" r="10" style="fill: red;"></circle>
<text x="2" dy="15" style="fill: white" v-if="total >= 10">
{{ total > 99 ? 99 : total }}
</text>
<text x="6" dy="15" style="fill: white" v-else-if="total > 0">
{{ total }}
</text>
</svg>
</template>
<script>
export default {
name: 'msgSvg',
props: {
total: {
required: true,
type: Number,
},
},
};
</script>

View File

@ -192,7 +192,7 @@ export const m = {
Use_Manual_Input:'Use Manual Input',
Hint: 'Hint',
Source: 'Source',
Auto_Remove_the_Blank_at_the_End_of_Code:'Auto Remove the Blank at the End of Code',
Auto_Remove_the_Blank_at_the_End_of_Code:'Automatically Remove Whitespace at The End of Each Line of Code',
Publish_the_Judging_Result_of_Test_Data:'Publish the Judging Result of Test Data',
Edit_Problem: 'Edit Problme',
Create_Problme: 'Create Problem',

View File

@ -102,6 +102,16 @@ export const m = {
Announcement_Content: '公告内容',
Announcement_visible: '是否可见',
Delete_Announcement_Tips:'你确实是否删除该公告?',
// /views/admin/general/SysNotice.vue
SysNotice: '系统通知',
Edit_Notice:'编辑通知',
Create_Notice:'创建通知',
Delete_Notice:'删除通知',
Notice_Title: '通知标题',
Notice_Content: '通知内容',
Notice_Push:'已推送',
Delete_Notice_Tips:'你确实是否删除该通知?',
// /views/admin/general/SystemConfig.vue
Website_Config:'网站设置',
@ -190,7 +200,7 @@ export const m = {
Use_Manual_Input:'使用手动输入',
Hint: '提示',
Source: '来源',
Auto_Remove_the_Blank_at_the_End_of_Code:'自动去除代码末尾空白符',
Auto_Remove_the_Blank_at_the_End_of_Code:'自动去除代码每行末尾空白符',
Publish_the_Judging_Result_of_Test_Data:'公开评测点数据结果',
Edit_Problem: '编辑题目',
Create_Problme: '创建题目',

View File

@ -435,5 +435,24 @@ export const m = {
Delete_Reply_Tips:'This operation will delete the reply. Do you want to continue?',
// /views/oj/message/message.vue
Message_Center:'Message Center',
No_Data:'No Data',
// /views/oj/message/UserMsg.vue
Msg_Total:'Total',
Msg_Messages:'messages',
DiscussMsg:'Discuss',
ReplyMsg:'Reply',
LikeMsg:'Likes',
SysMsg:'System',
MineMsg:'Mine',
Clean_All:'Clean All',
Action_Like_Discuss:'Praised My Comment',
Action_Like_Post:'Praised My Discussion Post',
Action_Discuss:'Commented on My Discussion Post',
Action_Reply:'Responded to My Comment',
From_Discussion_Post:'From Discussion Post',
From_the_Contest:'From the Contest',
Delete_Msg_Tips:'Are you sure you want to delete the message?'
}

View File

@ -417,7 +417,7 @@ export const m = {
// /components/oj/comment/comment.vue
Announcement_of_contest_Q_and_A_area:'比赛评论区公告',
Announcement_of_contest_Q_and_A_area_tips1:'请不要提问与比赛无关的问题,禁止灌水!',
Announcement_of_contest_Q_and_A_area_tips1:'请不要提问与比赛无关的问题,禁止灌水!',
Announcement_of_contest_Q_and_A_area_tips2:'比赛过程中,仅自己与比赛管理员的评论可见!',
Announcement_of_contest_Q_and_A_area_tips3:'比赛管理员评论不可回复,比赛结束评论恢复正常!',
Come_and_write_down_your_comments:'快来写下你的评论吧',
@ -436,4 +436,26 @@ export const m = {
Load_More:'加载更多',
Delete_Comment_Tips:'此操作将删除该评论及其所有回复, 是否继续?',
Delete_Reply_Tips:'此操作将删除该回复, 是否继续?',
// /views/oj/message/message.vue
Message_Center:'消息中心',
No_Data:'暂无数据',
// /views/oj/message/UserMsg.vue
Msg_Total:'共',
Msg_Messages:'条',
DiscussMsg:'评论我的',
ReplyMsg:'回复我的',
LikeMsg:'收到的赞',
SysMsg:'系统通知',
MineMsg:'我的消息',
Clean_All:'清空全部',
Action_Like_Discuss:'赞了我的评论',
Action_Like_Post:'赞了我的讨论帖',
Action_Discuss:'评论了我的讨论帖',
Action_Reply:'回复了我的评论',
From_Discussion_Post:'来自讨论帖',
From_the_Contest:'来自比赛',
Delete_Msg_Tips:'你是否确定要删除或清空消息?'
}

View File

@ -23,6 +23,9 @@ import DiscussionList from "@/views/oj/discussion/discussionList.vue"
import Discussion from "@/views/oj/discussion/discussion.vue"
import Introduction from "@/views/oj/about/Introduction.vue"
import Developer from "@/views/oj/about/Developer.vue"
import Message from "@/views/oj/message/message.vue"
import UserMsg from "@/views/oj/message/UserMsg.vue"
import SysMsg from "@/views/oj/message/SysMsg.vue"
import NotFound from "@/views/404.vue"
const ojRoutes = [
@ -206,6 +209,44 @@ const ojRoutes = [
meta: {title: 'Developer'},
component:Developer,
},
{
name:'Message',
path:'/message/',
component:Message,
meta: { requireAuth: false, title: 'Message' },
children: [
{
name: 'DiscussMsg',
path: 'discuss',
component: UserMsg,
meta: { requireAuth: false,title: 'Discuss Message' }
},
{
name: 'ReplyMsg',
path: 'reply',
component: UserMsg,
meta: { requireAuth: false,title: 'Reply Message' }
},
{
name: 'LikeMsg',
path: 'like',
component: UserMsg,
meta: { requireAuth: false,title: 'Like Message' }
},
{
name: 'SysMsg',
path: 'sys',
component: SysMsg,
meta: { requireAuth: false,title: 'System Message' }
},
{
name: 'MineMsg',
path: 'mine',
component: SysMsg,
meta: { requireAuth: false,title: 'Mine Message' }
},
]
},
{
path: '*',
meta: {title: '404'},

View File

@ -4,11 +4,19 @@ const state = {
userInfo: storage.get('userInfo'),
token: localStorage.getItem('token'),
loginFailNum:0,
unreadMessage:{
comment:0,
reply:0,
like:0,
sys:0,
mine:0
}
}
const getters = {
userInfo: state => state.userInfo || {},
token: state => state.token ||'',
unreadMessage:state => state.unreadMessage || {},
loginFailNum:state=>state.loginFailNum || 0,
isAuthenticated: (state, getters) => {
return !!getters.token
@ -60,6 +68,13 @@ const mutations = {
state.userInfo = {}
state.loginFailNum = 0
storage.clear()
},
updateUnreadMessageCount(state, {unreadMessage}){
state.unreadMessage = unreadMessage
},
substractUnreadMessageCount(state,{needSubstractMsg}){
// 负数也没关系
state.unreadMessage[needSubstractMsg.name] = state.unreadMessage[needSubstractMsg.name]-needSubstractMsg.num;
}
}
@ -75,6 +90,16 @@ const actions = {
clearUserInfoAndToken ({commit}) {
commit('clearUserInfoAndToken')
},
updateUnreadMessageCount({commit},unreadMessage){
commit('updateUnreadMessageCount', {
unreadMessage: unreadMessage
})
},
substractUnreadMessageCount({commit},needSubstractMsg){
commit('substractUnreadMessageCount', {
needSubstractMsg: needSubstractMsg
})
}
}
export default {

View File

@ -0,0 +1,338 @@
<template>
<div>
<el-card>
<div slot="header">
<span class="panel-title home-title">{{ $t('m.SysNotice') }}</span>
</div>
<div class="create">
<el-button
type="primary"
size="small"
@click="openNoticeDialog(null)"
icon="el-icon-plus"
>{{ $t('m.Create') }}</el-button
>
</div>
<div class="list">
<vxe-table
:loading="loading"
ref="table"
:data="noticeList"
auto-resize
stripe
>
<vxe-table-column min-width="50" field="id" title="ID">
</vxe-table-column>
<vxe-table-column
min-width="150"
field="title"
show-overflow
:title="$t('m.Notice_Title')"
>
</vxe-table-column>
<vxe-table-column
min-width="150"
field="gmtCreate"
:title="$t('m.Created_Time')"
>
<template v-slot="{ row }">
{{ row.gmtCreate | localtime }}
</template>
</vxe-table-column>
<vxe-table-column
min-width="150"
field="gmtModified"
:title="$t('m.Modified_Time')"
>
<template v-slot="{ row }">
{{ row.gmtModified | localtime }}
</template>
</vxe-table-column>
<vxe-table-column
min-width="150"
field="username"
show-overflow
:title="$t('m.Author')"
>
</vxe-table-column>
<vxe-table-column
min-width="100"
field="state"
:title="$t('m.Notice_Push')"
>
</vxe-table-column>
<vxe-table-column title="Option" min-width="150">
<template v-slot="row">
<el-tooltip
class="item"
effect="dark"
:content="$t('m.Edit_Notice')"
placement="top"
>
<el-button
icon="el-icon-edit-outline"
@click.native="openNoticeDialog(row.row)"
size="mini"
type="primary"
></el-button>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
:content="$t('m.Delete_Notice')"
placement="top"
>
<el-button
icon="el-icon-delete-solid"
@click.native="deleteNotice(row.row.id)"
size="mini"
type="danger"
></el-button>
</el-tooltip>
</template>
</vxe-table-column>
</vxe-table>
<div class="panel-options">
<el-pagination
v-if="!contestID"
class="page"
layout="prev, pager, next"
@current-change="currentChange"
:page-size="pageSize"
:total="total"
>
</el-pagination>
</div>
</div>
</el-card>
<!--编辑公告对话框-->
<el-dialog
:title="noticeDialogTitle"
:visible.sync="showEditNoticeDialog"
:fullscreen="true"
@open="onOpenEditDialog"
>
<el-form label-position="top" :model="notice">
<el-form-item :label="$t('m.Notice_Title')" required>
<el-input
v-model="notice.title"
:placeholder="$t('m.Notice_Title')"
class="title-input"
>
</el-input>
</el-form-item>
<el-form-item :label="$t('m.Notice_Content')" required>
<Editor :value.sync="notice.content"></Editor>
</el-form-item>
<div class="visible-box">
<span>{{ $t('m.Notice_visible') }}</span>
<el-switch
v-model="notice.status"
:active-value="0"
:inactive-value="1"
active-text=""
inactive-text=""
>
</el-switch>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="danger" @click.native="showEditNoticeDialog = false">{{
$t('m.Cancel')
}}</el-button>
<el-button type="primary" @click.native="submitNotice">{{
$t('m.OK')
}}</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import api from '@/common/api';
import myMessage from '@/common/message';
import { mapGetters } from 'vuex';
const Editor = () => import('@/components/admin/Editor.vue');
export default {
name: 'notice',
components: {
Editor,
},
data() {
return {
contestID: '',
//
showEditNoticeDialog: false,
//
noticeList: [],
//
pageSize: 15,
//
total: 0,
mode: 'create',
// (new | edit) model
notice: {
id: null,
title: '',
content: '',
status: 0,
uid: '',
},
//
noticeDialogTitle: 'Edit Notice',
// loading
loading: false,
//
currentPage: 0,
};
},
mounted() {
this.init();
},
methods: {
init() {
this.getNoticeList(1);
},
//
currentChange(page) {
this.currentPage = page;
if (this.contestID) {
this.getContestNoticeList(page);
} else {
this.getNoticeList(page);
}
},
getNoticeList(page) {
this.loading = true;
api.admin_getNoticeList(page, this.pageSize).then(
(res) => {
this.loading = false;
this.total = res.data.data.total;
this.noticeList = res.data.data.records;
},
(res) => {
this.loading = false;
}
);
},
//
onOpenEditDialog() {
// todo
// bug
setTimeout(() => {
if (document.createEvent) {
let event = document.createEvent('HTMLEvents');
event.initEvent('resize', true, true);
window.dispatchEvent(event);
} else if (document.createEventObject) {
window.fireEvent('onresize');
}
}, 0);
},
//
// MouseEvent
submitNotice(data = undefined) {
if (!data.id) {
data = this.notice;
}
let funcName =
this.mode === 'edit' ? 'admin_updateNotice' : 'admin_createNotice';
let = requestData = data;
api[funcName](requestData)
.then((res) => {
this.showEditNoticeDialog = false;
myMessage.success(this.$i18n.t('m.Post_successfully'));
this.init();
})
.catch();
},
//
deleteNotice(noticeId) {
this.$confirm(this.$i18n.t('m.Delete_Notice_Tips'), 'Warning', {
confirmButtonText: this.$i18n.t('m.OK'),
cancelButtonText: this.$i18n.t('m.Cancel'),
type: 'warning',
})
.then(() => {
// then
this.loading = true;
let funcName = 'admin_deleteNotice';
api[funcName](noticeId).then((res) => {
this.loading = true;
myMessage.success(res.data.msg);
this.init();
});
})
.catch(() => {
// catch
this.loading = false;
});
},
openNoticeDialog(row) {
this.showEditNoticeDialog = true;
if (row !== null) {
this.noticeDialogTitle = this.$i18n.t('m.Edit_Notice');
this.notice = Object.assign({}, row);
this.mode = 'edit';
} else {
this.noticeDialogTitle = this.$i18n.t('m.Create_Notice');
this.notice.title = '';
this.notice.status = 0;
this.notice.content = '';
this.notice.uid = this.userInfo.uid;
this.notice.username = this.userInfo.username;
this.mode = 'create';
}
},
handleVisibleSwitch(row) {
this.mode = 'edit';
this.submitNotice({
id: row.id,
title: row.title,
content: row.content,
status: row.status,
uid: row.uid,
});
},
},
watch: {
$route() {
this.init();
},
},
computed: {
...mapGetters(['userInfo']),
},
};
</script>
<style scoped>
.title-input {
margin-bottom: 20px;
}
.visible-box {
margin-top: 10px;
width: 205px;
float: left;
}
.visible-box span {
margin-right: 10px;
}
.el-form-item {
margin-bottom: 2px !important;
}
/deep/.el-dialog__body {
padding-top: 0 !important;
}
.create {
margin-bottom: 5px;
}
</style>

View File

@ -80,9 +80,9 @@
</div>
</div>
<p id="no-contest" v-show="contests.length == 0">
{{ $t('m.No_contest') }}
<el-empty :description="$t('m.No_contest')"></el-empty>
</p>
<ol id="contest-list">
<ol id="contest-list" v-loading="loading">
<li
v-for="contest in contests"
:key="contest.title"
@ -227,6 +227,7 @@ export default {
CONTEST_TYPE_REVERSE: CONTEST_TYPE_REVERSE,
acmSrc: require('@/assets/acm.jpg'),
oiSrc: require('@/assets/oi.jpg'),
loading: true,
};
},
mounted() {
@ -244,10 +245,17 @@ export default {
this.getContestList();
},
getContestList(page = 1) {
api.getContestList(page, this.limit, this.query).then((res) => {
this.contests = res.data.data.records;
this.total = res.data.data.total;
});
this.loading = true;
api.getContestList(page, this.limit, this.query).then(
(res) => {
this.contests = res.data.data.records;
this.total = res.data.data.total;
this.loading = false;
},
(err) => {
this.loading = false;
}
);
},
filterByChange() {
let query = Object.assign({}, this.query);

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="container">
<div class="container" v-loading="loading">
<div class="title-article" style="text-align: left">
<h1 class="title" id="sharetitle">
<span>{{ discussion.title }}</span>
@ -236,6 +236,7 @@ export default {
tagList: [],
content: '',
},
loading: false,
};
},
mounted() {
@ -246,14 +247,20 @@ export default {
init() {
this.routeName = this.$route.name;
this.discussionID = this.$route.params.discussionID || '';
api.getDiscussion(this.discussionID).then((res) => {
this.discussion = res.data.data;
this.changeDomTitle({ title: this.discussion.title });
this.$nextTick((_) => {
addCodeBtn();
});
});
this.loading = true;
api.getDiscussion(this.discussionID).then(
(res) => {
this.discussion = res.data.data;
this.changeDomTitle({ title: this.discussion.title });
this.$nextTick((_) => {
addCodeBtn();
});
this.loading = false;
},
(err) => {
this.loading = false;
}
);
},
getInfoByUsername(uid, username) {

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<el-row :gutter="20">
<el-col :md="18" :xs="24">
<el-col :md="18" :xs="24" v-loading="loading.discussion">
<div class="discussion-header">
<span style="padding: 16px;float:left;">
<el-breadcrumb separator-class="el-icon-arrow-right">
@ -32,170 +32,150 @@
></vxe-input
></span>
</div>
<div
class="title-article"
v-for="(discussion, index) in discussionList"
:key="index"
>
<el-card shadow="hover" class="list-card">
<span class="svg-top" v-if="discussion.topPriority">
<svg
t="1620283436433"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="10095"
width="48"
height="48"
>
<path
d="M989.9222626666667 444.3410103333334L580.1490096666668 34.909091333333336H119.41107066666666l870.511192 870.596525V444.3410103333334z"
fill="#F44336"
p-id="10096"
></path>
<path
d="M621.3675956666667 219.39846433333332l-43.832889-43.770828-126.663111 126.841535-32.826182-32.780929 126.663112-126.841535-43.734627-43.673859 26.739071-26.775273 120.396283 120.224324-26.741657 26.776565zM582.6055756666667 284.67587833333334c24.030384-24.065293 50.614303-36.636444 79.751758-37.71604 29.134869-1.07701 55.240404 9.903838 78.31402 32.945131 21.950061 21.91903 32.323232 46.86998 31.120808 74.851556s-13.257697 53.441939-36.167111 76.383677c-23.901091 23.934707-50.254869 36.406303-79.057455 37.41608-28.806465 1.012364-54.481455-9.739636-77.024969-32.252121-22.016-21.98497-32.689131-47.067798-32.014223-75.244606 0.672323-28.179394 12.365576-53.638465 35.077172-76.383677z m36.196849 32.57794c-14.921697 14.943677-23.517091 30.756202-25.783596 47.438869-2.269091 16.68396 2.880646 31.297939 15.441454 43.841939 12.825859 12.807758 27.34804 18.234182 43.566546 16.271515 16.217212-1.960081 31.985778-10.608485 47.303111-25.947798 15.976727-15.998707 25.133253-32.109899 27.46699-48.332283 2.333737-16.221091-2.813414-30.637253-15.441455-43.247192-12.827152-12.809051-27.67903-18.133333-44.558222-15.972848-16.879192 2.157899-32.877899 10.808889-47.994828 25.947798zM780.1276766666667 524.3048083333333l-53.476848 53.553131-32.726627-32.681374 153.400889-153.616808 52.858829 52.783839c38.213818 38.159515 41.146182 73.44097 8.79709 105.83402-15.71297 15.737535-34.076444 22.586182-55.086545 20.552404-21.012687-2.032485-39.97996-11.897535-56.905697-29.591273l-16.861091-16.833939z m74.572283-74.67701l-49.516606 49.586424 14.182141 14.161454c19.240081 19.211636 37.209212 20.455434 53.913859 3.728809 16.305131-16.329697 14.941091-34.002747-4.101172-53.016566L854.6999596666667 449.6277983333334z"
fill="#FFFFFF"
p-id="10097"
></path>
</svg>
</span>
<h1 class="article-hlink">
<a @click="toDiscussionDetail(discussion.id)">{{
discussion.title
}}</a>
<el-button
type="primary"
size="mini"
style="margin-left:5px;"
v-if="discussion.pid"
@click="
pushRouter(
null,
{ problemID: discussion.pid },
'ProblemDetails'
)
"
>{{ $t('m.Go_to_problem') }}</el-button
>
</h1>
<a
@click="toDiscussionDetail(discussion.id)"
class="article-hlink2"
>
<p>{{ discussion.description }}</p>
</a>
<div class="title-msg">
<span>
<a
@click="getInfoByUsername(discussion.uid, discussion.author)"
:title="discussion.author"
>
<avatar
:username="discussion.author"
:inline="true"
:size="24"
color="#FFF"
class="user-avatar"
:src="discussion.avatar"
></avatar>
<span class="pl">{{ discussion.author }}</span></a
>
<span
class="role-root role"
title="Super Administrator"
v-if="discussion.role == 'root'"
>SPA</span
>
<span
class="role-admin role"
title="Administrator"
v-if="discussion.role == 'admin'"
>ADM</span
<template v-if="discussionList.length > 0">
<div
class="title-article"
v-for="(discussion, index) in discussionList"
:key="index"
>
<el-card shadow="hover" class="list-card">
<span class="svg-top" v-if="discussion.topPriority">
<svg
t="1620283436433"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="10095"
width="48"
height="48"
>
<path
d="M989.9222626666667 444.3410103333334L580.1490096666668 34.909091333333336H119.41107066666666l870.511192 870.596525V444.3410103333334z"
fill="#F44336"
p-id="10096"
></path>
<path
d="M621.3675956666667 219.39846433333332l-43.832889-43.770828-126.663111 126.841535-32.826182-32.780929 126.663112-126.841535-43.734627-43.673859 26.739071-26.775273 120.396283 120.224324-26.741657 26.776565zM582.6055756666667 284.67587833333334c24.030384-24.065293 50.614303-36.636444 79.751758-37.71604 29.134869-1.07701 55.240404 9.903838 78.31402 32.945131 21.950061 21.91903 32.323232 46.86998 31.120808 74.851556s-13.257697 53.441939-36.167111 76.383677c-23.901091 23.934707-50.254869 36.406303-79.057455 37.41608-28.806465 1.012364-54.481455-9.739636-77.024969-32.252121-22.016-21.98497-32.689131-47.067798-32.014223-75.244606 0.672323-28.179394 12.365576-53.638465 35.077172-76.383677z m36.196849 32.57794c-14.921697 14.943677-23.517091 30.756202-25.783596 47.438869-2.269091 16.68396 2.880646 31.297939 15.441454 43.841939 12.825859 12.807758 27.34804 18.234182 43.566546 16.271515 16.217212-1.960081 31.985778-10.608485 47.303111-25.947798 15.976727-15.998707 25.133253-32.109899 27.46699-48.332283 2.333737-16.221091-2.813414-30.637253-15.441455-43.247192-12.827152-12.809051-27.67903-18.133333-44.558222-15.972848-16.879192 2.157899-32.877899 10.808889-47.994828 25.947798zM780.1276766666667 524.3048083333333l-53.476848 53.553131-32.726627-32.681374 153.400889-153.616808 52.858829 52.783839c38.213818 38.159515 41.146182 73.44097 8.79709 105.83402-15.71297 15.737535-34.076444 22.586182-55.086545 20.552404-21.012687-2.032485-39.97996-11.897535-56.905697-29.591273l-16.861091-16.833939z m74.572283-74.67701l-49.516606 49.586424 14.182141 14.161454c19.240081 19.211636 37.209212 20.455434 53.913859 3.728809 16.305131-16.329697 14.941091-34.002747-4.101172-53.016566L854.6999596666667 449.6277983333334z"
fill="#FFFFFF"
p-id="10097"
></path>
</svg>
</span>
<span class="pr pl"
><label class="fw"><i class="el-icon-chat-round"></i></label
><span>
<span class="hidden-xs-only"> {{ $t('m.Comment') }}:</span>
{{ discussion.commentNum }}</span
></span
>
<span class="pr"
><label class="fw"><i class="fa fa-thumbs-o-up"></i></label
><span>
<span class="hidden-xs-only"> {{ $t('m.Likes') }}:</span>
{{ discussion.likeNum }}</span
></span
>
<span class="pr"
><label class="fw"><i class="fa fa-eye"></i></label
><span>
<span class="hidden-xs-only"> {{ $t('m.Views') }}:</span>
{{ discussion.viewNum }}</span
></span
>
<span class="pr"
><label class="fw"><i class="el-icon-folder-opened"></i></label>
<a
<h1 class="article-hlink">
<a @click="toDiscussionDetail(discussion.id)">{{
discussion.title
}}</a>
<el-button
type="primary"
size="mini"
style="margin-left:5px;"
v-if="discussion.pid"
@click="
pushRouter(
{ cid: discussion.categoryId, onlyMine: query.onlyMine },
{ problemID: query.pid },
routeName
null,
{ problemID: discussion.pid },
'ProblemDetails'
)
"
>{{ $t('m.Go_to_problem') }}</el-button
>
{{ cidMapName[discussion.categoryId] }}</a
>
</span>
<span class="pr pl hidden-xs-only">
<label class="fw"><i class="fa fa-clock-o"></i></label
><span>
{{ $t('m.Release_Time') }}<el-tooltip
:content="discussion.gmtCreate | localtime"
placement="top"
>
<span>{{ discussion.gmtCreate | fromNow }}</span>
</el-tooltip></span
>
</span>
<el-dropdown
style="float:right;"
class="hidden-xs-only"
v-show="
isAuthenticated &&
(discussion.uid === userInfo.uid || isAdminRole)
"
@command="handleCommand"
</h1>
<a
@click="toDiscussionDetail(discussion.id)"
class="article-hlink2"
>
<span class="el-dropdown-link">
<i class="el-icon-more"></i>
<p>{{ discussion.description }}</p>
</a>
<div class="title-msg">
<span>
<a
@click="
getInfoByUsername(discussion.uid, discussion.author)
"
:title="discussion.author"
>
<avatar
:username="discussion.author"
:inline="true"
:size="24"
color="#FFF"
class="user-avatar"
:src="discussion.avatar"
></avatar>
<span class="pl">{{ discussion.author }}</span></a
>
<span
class="role-root role"
title="Super Administrator"
v-if="discussion.role == 'root'"
>SPA</span
>
<span
class="role-admin role"
title="Administrator"
v-if="discussion.role == 'admin'"
>ADM</span
>
</span>
<span class="pr pl"
><label class="fw"><i class="el-icon-chat-round"></i></label
><span>
<span class="hidden-xs-only"> {{ $t('m.Comment') }}:</span>
{{ discussion.commentNum }}</span
></span
>
<span class="pr"
><label class="fw"><i class="fa fa-thumbs-o-up"></i></label
><span>
<span class="hidden-xs-only"> {{ $t('m.Likes') }}:</span>
{{ discussion.likeNum }}</span
></span
>
<span class="pr"
><label class="fw"><i class="fa fa-eye"></i></label
><span>
<span class="hidden-xs-only"> {{ $t('m.Views') }}:</span>
{{ discussion.viewNum }}</span
></span
>
<span class="pr"
><label class="fw"
><i class="el-icon-folder-opened"></i
></label>
<a
@click="
pushRouter(
{
cid: discussion.categoryId,
onlyMine: query.onlyMine,
},
{ problemID: query.pid },
routeName
)
"
>
{{ cidMapName[discussion.categoryId] }}</a
>
</span>
<span class="pr pl hidden-xs-only">
<label class="fw"><i class="fa fa-clock-o"></i></label
><span>
{{ $t('m.Release_Time') }}<el-tooltip
:content="discussion.gmtCreate | localtime"
placement="top"
>
<span>{{ discussion.gmtCreate | fromNow }}</span>
</el-tooltip></span
>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
icon="el-icon-edit-outline"
:command="'edit:' + index"
v-show="discussion.uid === userInfo.uid"
>{{ $t('m.Edit') }}</el-dropdown-item
>
<el-dropdown-item
icon="el-icon-delete"
:command="'delete:' + index"
v-show="discussion.uid === userInfo.uid || isAdminRole"
>{{ $t('m.Delete') }}</el-dropdown-item
>
</el-dropdown-menu>
</el-dropdown>
<div class="hidden-sm-and-up">
<el-dropdown
style="float:right;margin-top:10px; "
style="float:right;"
class="hidden-xs-only"
v-show="
isAuthenticated &&
(discussion.uid === userInfo.uid || isAdminRole)
@ -221,19 +201,51 @@
</el-dropdown-menu>
</el-dropdown>
<span class="pr" style="float:right;margin-top:10px; "
><label class="fw"><i class="fa fa-clock-o"></i></label
><span> {{ discussion.gmtCreate | localtime }}</span></span
>
<div class="hidden-sm-and-up">
<el-dropdown
style="float:right;margin-top:10px; "
v-show="
isAuthenticated &&
(discussion.uid === userInfo.uid || isAdminRole)
"
@command="handleCommand"
>
<span class="el-dropdown-link">
<i class="el-icon-more"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
icon="el-icon-edit-outline"
:command="'edit:' + index"
v-show="discussion.uid === userInfo.uid"
>{{ $t('m.Edit') }}</el-dropdown-item
>
<el-dropdown-item
icon="el-icon-delete"
:command="'delete:' + index"
v-show="discussion.uid === userInfo.uid || isAdminRole"
>{{ $t('m.Delete') }}</el-dropdown-item
>
</el-dropdown-menu>
</el-dropdown>
<span class="pr" style="float:right;margin-top:10px; "
><label class="fw"><i class="fa fa-clock-o"></i></label
><span> {{ discussion.gmtCreate | localtime }}</span></span
>
</div>
</div>
</div>
</el-card>
</div>
</el-card>
</div>
</template>
<template v-else>
<el-empty :description="$t('m.No_Data')"></el-empty>
</template>
<Pagination
:total="total"
:page-size="limit"
:page-size="query.limit"
@on-change="changeRoute"
:current.sync="currentPage"
:current.sync="query.currentPage"
></Pagination>
</el-col>
<el-col :md="6" :xs="24">
@ -296,7 +308,7 @@
><i class="el-icon-folder-opened"></i> {{ $t('m.Category') }}</a
>
</h3>
<el-row>
<el-row v-loading="loading.category">
<el-col
:span="24"
class="category-item"
@ -401,8 +413,6 @@ export default {
data() {
return {
total: 0,
limit: 10,
currentPage: 1,
showEditDiscussionDialog: false,
discussion: {
id: null,
@ -427,22 +437,33 @@ export default {
keyword: '',
cid: '',
currentPage: 1,
limit: 15,
limit: 8,
pid: '',
onlyMine: false,
},
routeName: '',
loading: {
discussion: true,
category: true,
},
};
},
mounted() {
this.discussionDialogTitle = this.$i18n.t('m.Edit_Discussion');
api.getCategoryList().then((res) => {
this.categoryList = res.data.data;
for (let i = 0; i < this.categoryList.length; i++) {
this.cidMapName[this.categoryList[i].id] = this.categoryList[i].name;
this.loading.category = true;
api.getCategoryList().then(
(res) => {
this.categoryList = res.data.data;
for (let i = 0; i < this.categoryList.length; i++) {
this.cidMapName[this.categoryList[i].id] = this.categoryList[i].name;
}
this.loading.category = false;
this.init();
},
(err) => {
this.loading.category = false;
}
this.init();
});
);
},
methods: {
...mapActions(['changeDomTitle']),
@ -475,10 +496,17 @@ export default {
getDiscussionList() {
let queryParams = Object.assign({}, this.query);
api.getDiscussionList(this.limit, queryParams).then((res) => {
this.total = res.data.data.total;
this.discussionList = res.data.data.records;
});
this.loading.discussion = true;
api.getDiscussionList(this.limit, queryParams).then(
(res) => {
this.total = res.data.data.total;
this.discussionList = res.data.data.records;
this.loading.discussion = false;
},
(err) => {
this.loading.discussion = false;
}
);
},
changeRoute(page) {

View File

@ -0,0 +1,225 @@
<template>
<div class="msg-wrap" v-loading="loading">
<h3 class="msg-list-header">
<span class="ft">{{ $t('m.' + route_name) }}</span>
<span class="fr"
>{{ $t('m.Msg_Total') + ' ' + total + ' ' + $t('m.Msg_Messages') }}
<span class="clear-all" @click="deleteMsg()">{{
$t('m.Clean_All')
}}</span></span
>
</h3>
<template v-if="dataList.length > 0">
<el-card class="box-card" v-for="(item, index) in dataList" :key="index">
<div class="msg-list-item">
<span class="svg-lt" v-if="!item.state">
<svg
t="1633158277197"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4745"
width="32"
height="32"
>
<path
d="M512 322c-104.92 0-190 85.08-190 190s85.08 190 190 190 190-85.06 190-190-85.08-190-190-190z"
p-id="4746"
fill="#d81e06"
></path>
</svg>
</span>
<div class="top">
<span class="title">{{ item.title }}</span>
<span class="extra"
><el-tooltip
:content="item.gmtCreate | localtime"
placement="top"
>
<span>&nbsp;{{ item.gmtCreate | fromNow }}</span>
</el-tooltip></span
>
<span class="extra delete"
><i class="el-icon-delete" @click="deleteMsg(item.id)">
{{ $t('m.Delete') }}</i
></span
>
</div>
<div class="bottom">
<span
class="content markdown-body"
v-highlight
v-html="$markDown.render(item.content)"
>
</span>
</div>
</div>
</el-card>
</template>
<template v-else
><el-empty :description="$t('m.No_Data')"></el-empty>
</template>
<Pagination
:total="total"
:page-size="query.limit"
@on-change="changeRoute"
:current.sync="query.currentPage"
></Pagination>
</div>
</template>
<script>
import Pagination from '@/components/oj/common/Pagination';
import api from '@/common/api';
import myMessage from '@/common/message';
export default {
components: { Pagination },
data() {
return {
dataList: [],
loading: false,
total: 0,
query: { limit: 8, currentPage: 1 },
route_name: 'SysMsg',
};
},
created() {
this.route_name = this.$route.name;
},
mounted() {
let query = this.$route.query;
this.query.currentPage = parseInt(query.currentPage) || 1;
if (this.query.currentPage < 1) {
this.query.currentPage = 1;
}
this.getMsgList();
},
methods: {
getMsgList() {
let queryParams = Object.assign({}, this.query);
this.loading = true;
api.getMsgList(this.route_name, queryParams).then(
(res) => {
this.total = res.data.data.total;
this.dataList = res.data.data.records;
this.loading = false;
this.substractUnreadMsgNum();
},
(err) => {
this.loading = false;
}
);
},
changeRoute(page) {
this.query.currentPage = page;
this.getMsgList();
},
goMsgSourceUrl(url) {
this.$router.push({
path: url,
});
},
getInfoByUsername(uid, username) {
this.$router.push({
path: '/user-home',
query: { uid, username },
});
},
deleteMsg(id = undefined) {
this.$confirm(this.$i18n.t('m.Delete_Msg_Tips'), 'Tips', {
confirmButtonText: this.$i18n.t('m.OK'),
cancelButtonText: this.$i18n.t('m.Cancel'),
type: 'warning',
}).then(() => {
api.cleanMsg(this.route_name, id).then((res) => {
myMessage.success(this.$i18n.t('m.Delete_successfully'));
this.getMsgList();
});
});
},
substractUnreadMsgNum() {
let countName;
switch (this.route_name) {
case 'SysMsg':
countName = 'sys';
break;
case 'MineMsg':
countName = 'mine';
break;
}
let needSubstractMsg = {
name: countName,
num: this.limit,
};
this.$store.dispatch('substractUnreadMessageCount', needSubstractMsg);
},
},
};
</script>
<style scoped>
.box-card {
margin-bottom: 15px;
position: relative;
}
.clear-all {
cursor: pointer;
color: #409eff;
}
.clear-all:hover {
color: red;
font-weight: bolder;
}
.msg-wrap {
padding: 20px;
padding-top: 0px;
overflow: hidden;
}
@media only screen and (max-width: 767px) {
.msg-wrap {
padding: 0px;
}
}
.msg-list-header {
height: 35px;
border-bottom: 3px solid #eff3f5;
position: relative;
top: -10px;
}
.svg-lt {
position: absolute;
top: 0px;
right: 0px;
}
.fl {
float: left;
}
.fr {
float: right;
}
.msg-list-item {
line-height: 24px;
}
.title {
color: #333;
font-weight: 700;
}
.extra {
color: #999;
font-size: 12px;
line-height: 22px;
margin: 0 8px;
}
.bottom {
color: #666;
padding-left: 8px;
}
.text {
word-break: break-word;
}
.delete:hover {
cursor: pointer;
color: red;
font-weight: bolder;
}
</style>

View File

@ -0,0 +1,343 @@
<template>
<div class="msg-wrap" v-loading="loading">
<h3 class="msg-list-header">
<span class="ft">{{ $t('m.' + route_name) }}</span>
<span class="fr"
>{{ $t('m.Msg_Total') + ' ' + total + ' ' + $t('m.Msg_Messages') }}
<span class="clear-all" @click="deleteMsg()">{{
$t('m.Clean_All')
}}</span></span
>
</h3>
<template v-if="dataList.length > 0">
<el-card class="box-card" v-for="(item, index) in dataList" :key="index">
<div class="msg-list-item">
<span class="svg-lt" v-if="!item.state">
<svg
t="1633158277197"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4745"
width="32"
height="32"
>
<path
d="M512 322c-104.92 0-190 85.08-190 190s85.08 190 190 190 190-85.06 190-190-85.08-190-190-190z"
p-id="4746"
fill="#d81e06"
></path>
</svg>
</span>
<span
@click="getInfoByUsername(item.senderId, item.senderUsername)"
style="cursor: pointer;"
>
<avatar
:username="item.senderUsername"
:inline="true"
:size="40"
color="#FFF"
:src="item.senderAvatar"
:title="item.senderUsername"
></avatar>
</span>
<div class="title">
<div>
<span
style="margin-right:3px;"
class="user-name"
@click="getInfoByUsername(item.senderId, item.senderUsername)"
:title="item.senderUsername"
>{{ item.senderUsername }}</span
>
<span class="msg-action">
{{ $t('m.Action_' + item.action) }}
</span>
</div>
<div @click="goMsgSourceUrl(item.url)" style="cursor: pointer;">
<div
class="content"
v-if="item.sourceContent != null"
v-html="item.sourceContent"
></div>
<div
class="orginal-reply"
v-if="item.quoteContent != null"
v-html="item.quoteContent"
></div>
</div>
<div class="extra-info">
<span
><i class="el-icon-time">
<el-tooltip
:content="item.gmtCreate | localtime"
placement="top"
>
<span>&nbsp;{{ item.gmtCreate | fromNow }}</span>
</el-tooltip></i
></span
>
<span class="delete" @click="deleteMsg(item.id)"
><i class="el-icon-delete"> {{ $t('m.Delete') }}</i></span
>
</div>
</div>
</div>
<div class="link-discussion">
<span
>{{
item.sourceType == 'Discussion'
? $t('m.From_Discussion_Post')
: $t('m.From_the_Contest')
}}
<span class="title" @click="goMsgSourceUrl(item.url)"
>{{ item.sourceTitle }}</span
></span
>
</div>
</el-card>
</template>
<template v-else
><el-empty :description="$t('m.No_Data')"></el-empty>
</template>
<Pagination
:total="total"
:page-size="query.limit"
@on-change="changeRoute"
:current.sync="query.currentPage"
></Pagination>
</div>
</template>
<script>
import Avatar from 'vue-avatar';
import api from '@/common/api';
import myMessage from '@/common/message';
import Pagination from '@/components/oj/common/Pagination';
export default {
components: { Avatar, Pagination },
data() {
return {
dataList: [],
query: {
currentPage: 1,
limit: 6,
},
loading: false,
total: 0,
route_name: 'DiscussMsg',
};
},
created() {
this.route_name = this.$route.name;
},
mounted() {
let query = this.$route.query;
this.query.currentPage = parseInt(query.currentPage) || 1;
if (this.query.currentPage < 1) {
this.query.currentPage = 1;
}
this.getMsgList();
},
methods: {
getMsgList() {
let queryParams = Object.assign({}, this.query);
this.loading = true;
api.getMsgList(this.route_name, queryParams).then(
(res) => {
this.total = res.data.data.total;
this.dataList = res.data.data.records;
this.loading = false;
this.substractUnreadMsgNum();
},
(err) => {
this.loading = false;
}
);
},
changeRoute(page) {
this.query.currentPage = page;
this.getMsgList();
},
goMsgSourceUrl(url) {
this.$router.push({
path: url,
});
},
getInfoByUsername(uid, username) {
this.$router.push({
path: '/user-home',
query: { uid, username },
});
},
deleteMsg(id = undefined) {
this.$confirm(this.$i18n.t('m.Delete_Msg_Tips'), 'Tips', {
confirmButtonText: this.$i18n.t('m.OK'),
cancelButtonText: this.$i18n.t('m.Cancel'),
type: 'warning',
}).then(() => {
api.cleanMsg(this.route_name, id).then((res) => {
myMessage.success(this.$i18n.t('m.Delete_successfully'));
this.getMsgList();
});
});
},
substractUnreadMsgNum() {
let countName;
switch (this.route_name) {
case 'DiscussMsg':
countName = 'comment';
break;
case 'ReplyMsg':
countName = 'reply';
break;
case 'LikeMsg':
countName = 'like';
break;
}
let needSubstractMsg = {
name: countName,
num: this.limit,
};
this.$store.dispatch('substractUnreadMessageCount', needSubstractMsg);
},
},
};
</script>
<style scoped>
.box-card {
margin-bottom: 15px;
position: relative;
}
.clear-all {
cursor: pointer;
color: #409eff;
}
.clear-all:hover {
color: red;
font-weight: bolder;
}
.msg-wrap {
padding: 20px;
padding-top: 0px;
overflow: hidden;
}
@media only screen and (max-width: 767px) {
.msg-wrap {
padding: 0px;
}
}
.msg-list-header {
height: 35px;
border-bottom: 3px solid #eff3f5;
position: relative;
top: -10px;
}
.svg-lt {
position: absolute;
top: 0px;
right: 0px;
}
.fl {
float: left;
}
.fr {
float: right;
}
.msg-list-item {
display: flex;
}
.msg-list-item .title {
color: #99a;
font-size: 16px;
margin-left: 13px;
}
.msg-list-item .title .content {
color: #222;
word-break: break-word;
margin: 10px 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 20px;
max-height: 2.6em;
}
.user-name {
color: #666;
font-weight: bold;
}
.user-name:hover {
cursor: pointer;
color: #409eff;
}
.msg-action {
font-size: 16px;
margin-left: 5px;
}
.msg-list-item .orginal-reply {
color: #999;
border-left: 2px solid #e7e7e7;
margin: 8px 0 5px;
padding-left: 4px;
font-size: 12px;
line-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
font-size: 12px;
line-height: 16px;
max-height: 2.6em;
}
.msg-list-item .extra-info {
color: #999;
font-size: 12px;
line-height: 30px;
}
.msg-list-item .extra-info span {
margin-right: 10px;
}
.msg-list-item .extra-info .delete:hover {
cursor: pointer;
color: red;
}
.link-discussion {
color: #999;
font-size: 15px;
text-align: center;
}
.link-discussion .title {
color: #409eff;
font-weight: 700;
cursor: pointer;
}
@media only screen and (max-width: 767px) {
.link-discussion {
text-align: left;
}
.msg-action {
font-size: 13px;
margin-left: 0px;
display: block;
}
.msg-list-item .title .content {
margin-left: -47px;
}
.msg-list-item .extra-info {
margin-left: -47px;
}
.msg-list-item .orginal-reply {
margin-left: -47px;
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div>
<el-alert
type="success"
:closable="false"
center
class="msg-title"
effect="dark"
>
<template slot="title">
<span
><i class="el-icon-s-promotion">
{{ $t('m.Message_Center') }}</i
></span
>
</template>
</el-alert>
<el-tabs
tab-position="left"
type="border-card"
style="min-height: 500px;"
v-model="route_name"
@tab-click="handleRouter"
>
<el-tab-pane name="DiscussMsg">
<span slot="label">
<span>{{ $t('m.DiscussMsg') }}</span>
<span style=" margin-left: 2px;" v-if="unreadMessage.comment > 0">
<MsgSvg :total="unreadMessage.comment"></MsgSvg>
</span>
</span>
<transition name="fadeInUp" mode="out-in">
<router-view v-if="route_name === 'DiscussMsg'"></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="ReplyMsg">
<span slot="label">
<span>{{ $t('m.ReplyMsg') }}</span>
<span style=" margin-left: 2px;" v-if="unreadMessage.reply > 0">
<MsgSvg :total="unreadMessage.reply"></MsgSvg>
</span>
</span>
<transition name="fadeInUp" mode="out-in">
<router-view v-if="route_name === 'ReplyMsg'"></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="LikeMsg">
<span slot="label">
<span>{{ $t('m.LikeMsg') }}</span>
<span style=" margin-left: 2px;" v-if="unreadMessage.like > 0">
<MsgSvg :total="unreadMessage.like"></MsgSvg>
</span>
</span>
<transition name="fadeInUp" mode="out-in">
<router-view v-if="route_name === 'LikeMsg'"></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="SysMsg">
<span slot="label">
<span>{{ $t('m.SysMsg') }}</span>
<span style=" margin-left: 2px;" v-if="unreadMessage.sys > 0">
<MsgSvg :total="unreadMessage.sys"></MsgSvg>
</span>
</span>
<transition name="fadeInUp" mode="out-in">
<router-view v-if="route_name === 'SysMsg'"></router-view>
</transition>
</el-tab-pane>
<el-tab-pane name="MineMsg">
<span slot="label">
<span>{{ $t('m.MineMsg') }}</span>
<span style=" margin-left: 2px;" v-if="unreadMessage.mine > 0">
<MsgSvg :total="unreadMessage.mine"></MsgSvg>
</span>
</span>
<transition name="fadeInUp" mode="out-in">
<router-view v-if="route_name === 'MineMsg'"></router-view>
</transition>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import MsgSvg from '@/components/oj/msg/msgSvg';
export default {
components: {
MsgSvg,
},
data() {
return {
route_name: 'DiscussMsg',
};
},
mounted() {
this.route_name = this.$route.name;
if (this.route_name === 'Message') {
this.route_name = 'DiscussMsg';
}
this.$router.push({ name: this.route_name });
},
methods: {
handleRouter(tab) {
let name = tab.name;
if (name !== this.$route.name) {
this.$router.push({ name: name });
}
},
},
computed: {
...mapGetters(['unreadMessage']),
},
};
</script>
<style scoped>
.msg-title {
background-image: linear-gradient(135deg, #2afadf 10%, #4c83ff 100%);
}
/deep/.el-alert__title {
font-size: 18px !important;
line-height: 18px !important;
}
/deep/.el-tabs__item {
text-align: center !important;
}
/deep/.el-tabs__item {
padding: 0 40px;
line-height: 53px;
height: 53px;
font-weight: 700;
}
/deep/.el-card__body {
padding: 15px;
padding-bottom: 10px;
}
@media only screen and (max-width: 767px) {
/deep/.el-tabs__item {
padding: 0 10px;
}
/deep/.el-tabs__content {
padding: 12px;
padding-left: 0px !important;
}
}
</style>

View File

@ -118,13 +118,14 @@
}}MB</span
><br />
</template>
<span
>{{ $t('m.Level') }}{{
PROBLEM_LEVEL[problemData.problem.difficulty]['name']
}}</span
>
<br />
<template v-if="problemData.problem.difficulty != null">
<span
>{{ $t('m.Level') }}{{
PROBLEM_LEVEL[problemData.problem.difficulty]['name']
}}</span
>
<br />
</template>
<template v-if="problemData.problem.type == 1">
<span
>{{ $t('m.Score') }}{{ problemData.problem.ioScore }}

View File

@ -237,21 +237,25 @@
<div slot="header" style="text-align: center;">
<span class="taglist-title">{{ OJName + ' ' + $t('m.Tags') }}</span>
</div>
<el-button
v-for="tag in tagList"
:key="tag.id"
@click="filterByTag(tag.id)"
type="ghost"
:disabled="query.tagId == tag.id"
size="mini"
class="tag-btn"
>{{ tag.name }}
</el-button>
<el-button long id="pick-one" @click="pickone">
<i class="fa fa-random"></i>
{{ $t('m.Pick_a_random_question') }}
</el-button>
<template v-if="tagList.length > 0" v-loading="loadings.tag">
<el-button
v-for="tag in tagList"
:key="tag.id"
@click="filterByTag(tag.id)"
type="ghost"
:disabled="query.tagId == tag.id"
size="mini"
class="tag-btn"
>{{ tag.name }}
</el-button>
<el-button long id="pick-one" @click="pickone">
<i class="fa fa-random"></i>
{{ $t('m.Pick_a_random_question') }}
</el-button>
</template>
<template v-else
><el-empty :description="$t('m.No_Data')"></el-empty>
</template>
</el-card>
</el-col>
</el-row>
@ -460,6 +464,7 @@ export default {
if (oj == 'Mine') {
oj = 'ME';
}
this.loadings.tag = true;
api.getProblemTagList(oj).then(
(res) => {
this.tagList = res.data.data;

View File

@ -35,7 +35,7 @@ const cdn = {
"https://cdn.bootcdn.net/ajax/libs/vue-router/3.2.0/vue-router.min.js",
"https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js",
"https://cdn.bootcdn.net/ajax/libs/vuex/3.5.1/vuex.min.js",
"https://cdn.bootcdn.net/ajax/libs/element-ui/2.14.0/index.min.js",
"https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.3/index.min.js",
"https://cdn.bootcdn.net/ajax/libs/highlight.js/10.3.2/highlight.min.js",
"https://cdn.jsdelivr.net/npm/xe-utils",
"https://cdn.jsdelivr.net/npm/vxe-table@2.9.26",

View File

@ -132,3 +132,82 @@ CALL Add_contest_print ;
DROP PROCEDURE Add_contest_print;
/*
* 2021.10.04
*/
DROP PROCEDURE
IF EXISTS Add_msg_table;
DELIMITER $$
CREATE PROCEDURE Add_msg_table ()
BEGIN
IF NOT EXISTS (
SELECT
1
FROM
information_schema.`COLUMNS`
WHERE
table_name = 'msg_remind'
) THEN
CREATE TABLE `admin_sys_notice` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL COMMENT '标题',
`content` longtext COMMENT '内容',
`type` varchar(255) DEFAULT NULL COMMENT '发给哪些用户类型',
`state` tinyint(1) DEFAULT '0' COMMENT '是否已拉取给用户',
`recipient_id` varchar(32) DEFAULT NULL COMMENT '接受通知的用户id',
`admin_id` varchar(32) DEFAULT NULL COMMENT '发送通知的管理员id',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `recipient_id` (`recipient_id`),
KEY `admin_id` (`admin_id`),
CONSTRAINT `admin_sys_notice_ibfk_1` FOREIGN KEY (`recipient_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `admin_sys_notice_ibfk_2` FOREIGN KEY (`admin_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `msg_remind` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`action` varchar(255) NOT NULL COMMENT '动作类型如点赞讨论帖Like_Post、点赞评论Like_Discuss、评论Discuss、回复Reply等',
`source_id` int(10) unsigned DEFAULT NULL COMMENT '消息来源id讨论id或比赛id',
`source_type` varchar(255) DEFAULT NULL COMMENT '事件源类型:''Discussion''''Contest''',
`source_content` varchar(255) DEFAULT NULL COMMENT '事件源的内容,比如回复的内容,评论的帖子标题等等',
`quote_id` int(10) unsigned DEFAULT NULL COMMENT '事件引用上一级评论或回复id',
`quote_type` varchar(255) DEFAULT NULL COMMENT '事件引用上一级的类型Comment、Reply',
`url` varchar(255) DEFAULT NULL COMMENT '事件所发生的地点链接 url',
`state` tinyint(1) DEFAULT '0' COMMENT '是否已读',
`sender_id` varchar(32) DEFAULT NULL COMMENT '操作者的id',
`recipient_id` varchar(32) DEFAULT NULL COMMENT '接受消息的用户id',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `sender_id` (`sender_id`),
KEY `recipient_id` (`recipient_id`),
CONSTRAINT `msg_remind_ibfk_1` FOREIGN KEY (`sender_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `msg_remind_ibfk_2` FOREIGN KEY (`recipient_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_sys_notice` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`sys_notice_id` bigint(20) unsigned DEFAULT NULL COMMENT '系统通知的id',
`recipient_id` varchar(32) DEFAULT NULL COMMENT '接受通知的用户id',
`type` varchar(255) DEFAULT NULL COMMENT '消息类型系统通知sys、我的信息mine',
`state` tinyint(1) DEFAULT '0' COMMENT '是否已读',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '读取时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `sys_notice_id` (`sys_notice_id`),
KEY `recipient_id` (`recipient_id`),
CONSTRAINT `user_sys_notice_ibfk_1` FOREIGN KEY (`sys_notice_id`) REFERENCES `admin_sys_notice` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `user_sys_notice_ibfk_2` FOREIGN KEY (`recipient_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
END
IF ; END$$
DELIMITER ;
CALL Add_msg_table;
DROP PROCEDURE Add_msg_table;

View File

@ -776,6 +776,69 @@ CREATE TABLE `remote_judge_account` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `admin_sys_notice`;
CREATE TABLE `admin_sys_notice` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL COMMENT '标题',
`content` longtext COMMENT '内容',
`type` varchar(255) DEFAULT NULL COMMENT '发给哪些用户类型',
`state` tinyint(1) DEFAULT '0' COMMENT '是否已拉取给用户',
`recipient_id` varchar(32) DEFAULT NULL COMMENT '接受通知的用户id',
`admin_id` varchar(32) DEFAULT NULL COMMENT '发送通知的管理员id',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `recipient_id` (`recipient_id`),
KEY `admin_id` (`admin_id`),
CONSTRAINT `admin_sys_notice_ibfk_1` FOREIGN KEY (`recipient_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `admin_sys_notice_ibfk_2` FOREIGN KEY (`admin_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `msg_remind` */
DROP TABLE IF EXISTS `msg_remind`;
CREATE TABLE `msg_remind` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`action` varchar(255) NOT NULL COMMENT '动作类型如点赞讨论帖Like_Post、点赞评论Like_Discuss、评论Discuss、回复Reply等',
`source_id` int(10) unsigned DEFAULT NULL COMMENT '消息来源id讨论id或比赛id',
`source_type` varchar(255) DEFAULT NULL COMMENT '事件源类型:''Discussion''''Contest''',
`source_content` varchar(255) DEFAULT NULL COMMENT '事件源的内容,比如回复的内容,评论的帖子标题等等',
`quote_id` int(10) unsigned DEFAULT NULL COMMENT '事件引用上一级评论或回复id',
`quote_type` varchar(255) DEFAULT NULL COMMENT '事件引用上一级的类型Comment、Reply',
`url` varchar(255) DEFAULT NULL COMMENT '事件所发生的地点链接 url',
`state` tinyint(1) DEFAULT '0' COMMENT '是否已读',
`sender_id` varchar(32) DEFAULT NULL COMMENT '操作者的id',
`recipient_id` varchar(32) DEFAULT NULL COMMENT '接受消息的用户id',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `sender_id` (`sender_id`),
KEY `recipient_id` (`recipient_id`),
CONSTRAINT `msg_remind_ibfk_1` FOREIGN KEY (`sender_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `msg_remind_ibfk_2` FOREIGN KEY (`recipient_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `user_sys_notice` */
DROP TABLE IF EXISTS `user_sys_notice`;
CREATE TABLE `user_sys_notice` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`sys_notice_id` bigint(20) unsigned DEFAULT NULL COMMENT '系统通知的id',
`recipient_id` varchar(32) DEFAULT NULL COMMENT '接受通知的用户id',
`type` varchar(255) DEFAULT NULL COMMENT '消息类型系统通知sys、我的信息mine',
`state` tinyint(1) DEFAULT '0' COMMENT '是否已读',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '读取时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `sys_notice_id` (`sys_notice_id`),
KEY `recipient_id` (`recipient_id`),
CONSTRAINT `user_sys_notice_ibfk_1` FOREIGN KEY (`sys_notice_id`) REFERENCES `admin_sys_notice` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `user_sys_notice_ibfk_2` FOREIGN KEY (`recipient_id`) REFERENCES `user_info` (`uuid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/* Trigger structure for table `contest` */