Merge branch 'blossom-editor:dev' into dev

This commit is contained in:
Tianjiu 2024-02-07 21:39:53 +08:00 committed by GitHub
commit 106ae03a3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
184 changed files with 3083 additions and 927 deletions

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>blossom-backend</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -1,9 +1,12 @@
package com.blossom.backend.base.auth;
import cn.hutool.core.util.StrUtil;
import com.blossom.backend.base.auth.pojo.LoginReq;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.auth.pojo.AccessToken;
import com.blossom.backend.base.auth.pojo.KickOutReq;
import com.blossom.backend.base.auth.pojo.LoginReq;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.pojo.R;
import lombok.AllArgsConstructor;
@ -52,6 +55,18 @@ public class AuthController {
return R.ok();
}
/**
* 踢出用户
*
* @since 1.13.0
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("kickout")
public R<?> kickout(@RequestBody KickOutReq req) {
authService.kickout(req.getUserId());
return R.ok();
}
/**
* 检查 Token 状态
*/

View File

@ -46,6 +46,7 @@ public class AuthService extends AbstractAuthService {
AuthException.throwBy(StrUtil.isBlank(login.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
UserEntity user = userService.selectByUsername(login.getUsername());
AuthException.throwBy(ObjUtil.isNull(user), AuthRCode.USERNAME_OR_PWD_FAULT);
AuthException.throwBy(user.getDelTime() == null || !user.getDelTime().equals(0L), AuthRCode.USER_NOT_ENABLED);
AuthException.throwBy(!passwordEncoder.matches(login.getPassword() + user.getSalt(), user.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
fillUserDetail(accessToken, user);
}
@ -59,6 +60,15 @@ public class AuthService extends AbstractAuthService {
tokenRepository.remove(token);
}
/**
* 踢出用户的所有令牌
*
* @param userId 用户ID
*/
public void kickout(Long userId) {
tokenRepository.removeAll(userId);
}
/**
* 用户注册
*/

View File

@ -0,0 +1,19 @@
package com.blossom.backend.base.auth.annotation;
import com.blossom.backend.base.user.UserTypeEnum;
import java.lang.annotation.*;
/**
* 接口校验用户类型
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthUserType {
/**
* 用户的类型, 接口只允许该类型用户调用
*/
UserTypeEnum value();
}

View File

@ -13,6 +13,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
@ -42,7 +43,7 @@ public class CaffeineTokenRepository implements TokenRepository {
.initialCapacity(1000)
.expireAfterWrite(client.getDuration(), TimeUnit.SECONDS)
.removalListener((String key, AccessToken value, RemovalCause cause) ->
log.info("Token [" + key + "] 被删除")
log.info("Token [" + key + "] has been deleted")
)
.build();
@ -50,9 +51,10 @@ public class CaffeineTokenRepository implements TokenRepository {
.initialCapacity(1000)
.expireAfterWrite(client.getDuration(), TimeUnit.SECONDS)
.removalListener((String userId, String token, RemovalCause cause) ->
log.info("Unique Token(userId) [" + userId + "] 被删除")
log.info("Unique Token(userId) [" + userId + "] has been deleted")
)
.build();
}
@Override
@ -78,6 +80,17 @@ public class CaffeineTokenRepository implements TokenRepository {
tokenCache.invalidate(token);
}
@Override
public void removeAll(Long userId) {
uniqueTokenCache.invalidate(userId);
Map<String, AccessToken> maps = tokenCache.asMap();
maps.forEach((k, t) -> {
if (t.getUserId().equals(userId)) {
tokenCache.invalidate(k);
}
});
}
@Override
public void saveUniqueToken(AccessToken accessToken) {
if (accessToken == null || StrUtil.isBlank(accessToken.getToken())) {

View File

@ -27,7 +27,7 @@ public enum AuthRCode implements IRCode {
*/
USERNAME_OR_PWD_FAULT ("AUTH-40004", "用户名或密码错误","用户名或密码错误, 或用户名不存在。"),
CAPTCHA_FAULT ("AUTH-40005", "验证码错误","验证码错误, 或手机号不存在。"),
USER_NOT_ENABLED ("AUTH-40010", "用户已禁用, 暂时无法登录","用户已禁用, 暂时无法登录。"),
USER_NOT_ENABLED ("AUTH-40010", "您的账户已被已禁用, 暂时无法登录","您的账户已被已禁用, 暂时无法登录。"),
/**
* 401: 未经过认证

View File

@ -2,8 +2,10 @@ package com.blossom.backend.base.auth.interceptor;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.auth.exception.AuthException;
import com.blossom.backend.base.auth.exception.AuthRCode;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.common.base.exception.XzException400;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.web.method.HandlerMethod;
@ -14,7 +16,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 判断用户的类型, 只读用户只允许发送 get 请求
* 接口对于用户的校验
*/
@Slf4j
public class UserTypeInterceptor implements HandlerInterceptor {
@ -28,7 +30,7 @@ public class UserTypeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 忽略静态资源处理器
if(handler instanceof ResourceHttpRequestHandler) {
if (handler instanceof ResourceHttpRequestHandler) {
return true;
}
@ -40,12 +42,25 @@ public class UserTypeInterceptor implements HandlerInterceptor {
if (isIgnore) {
return true;
}
// GET请求校验
if (HttpMethod.GET.name().equals(request.getMethod())) {
return true;
// 校验接口允许的用户类型
boolean isCheckUserType = handlerMethod.hasMethodAnnotation(AuthUserType.class);
if (isCheckUserType) {
AuthUserType userType = handlerMethod.getMethodAnnotation(AuthUserType.class);
if (userType != null) {
UserTypeEnum type = userType.value();
if (!type.getType().equals(AuthContext.getType())) {
throw new AuthException(AuthRCode.PERMISSION_DENIED);
}
return true;
}
}
// 只读账号不非 GET 请求
if (UserTypeEnum.READONLY.getType().equals(AuthContext.getType()) && !HttpMethod.GET.name().equals(request.getMethod())) {
throw new AuthException(AuthRCode.PERMISSION_DENIED.getCode(), "您的账号为只读账号, 无法使用该功能");
}
XzException400.throwBy(UserTypeEnum.READONLY.getType().equals(AuthContext.getType()), "您的账号为只读账号, 无法使用该功能");
return true;
}
}

View File

@ -31,6 +31,10 @@ public class JWTTokenRepository implements TokenRepository {
}
@Override
public void removeAll(Long userId) {
}
@Override
public void saveUniqueToken(AccessToken accessToken) {

View File

@ -0,0 +1,18 @@
package com.blossom.backend.base.auth.pojo;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 踢出用户
*/
@Data
public class KickOutReq {
/**
* 用户ID
*/
@NotNull(message = "userId 为必填项")
private Long userId;
}

View File

@ -31,6 +31,13 @@ public interface TokenRepository {
*/
void remove(String token);
/**
* 删除某个用户的所有 token
*
* @param userId 用户ID
*/
void removeAll(Long userId);
/**
* 保存唯一生效的 token 对象
*

View File

@ -1,7 +1,11 @@
package com.blossom.backend.base.paramu;
import cn.hutool.core.util.ObjUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.paramu.pojo.UserParamUpdReq;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.pojo.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -45,6 +49,20 @@ public class UserParamController {
return R.ok(baseService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values()));
}
/**
* 修改用户参数
*
* @apiNote 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/upd/admin")
public R<Map<String, String>> updByAdmin(@Validated @RequestBody UserParamUpdReq req) {
XzException400.throwBy(ObjUtil.isNull(req.getUserId()), "用户ID为必填项");
baseService.update(req);
baseService.refresh();
return R.ok(baseService.selectMap(req.getUserId(), true, UserParamEnum.values()));
}
/**
* 刷新用户配置
*/

View File

@ -38,6 +38,11 @@ public enum UserParamEnum {
* 更多链接 JSON
*/
WEB_BLOG_LINKS(false, 0, ""),
/**
* 博客端专题特殊形式, 0:false;1:
* @since 1.13.0
*/
WEB_BLOG_SUBJECT_TITLE(false, 0, "0"),
;
/**

View File

@ -44,4 +44,10 @@ public interface UserParamMapper extends BaseMapper<UserParamEntity> {
* 新增参数
*/
int insertByUserId(UserParamEntity param);
/**
* 删除参数
* @param userId 用户ID
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -2,6 +2,7 @@ package com.blossom.backend.base.sys;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.param.pojo.ParamUpdReq;
@ -63,6 +64,7 @@ public class SysController {
/**
* 修改系统参数
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/param/upd")
public R<Map<String, String>> upd(@Validated @RequestBody ParamUpdReq req) {
if (!UserTypeEnum.ADMIN.getType().equals(AuthContext.getType())) {
@ -76,6 +78,7 @@ public class SysController {
/**
* 刷新系统配置
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/param/refresh")
public R<?> paramRefresh() {
paramService.refresh();

View File

@ -2,7 +2,9 @@ package com.blossom.backend.base.user;
import cn.hutool.core.util.ObjUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.AuthService;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.paramu.UserParamEnum;
@ -12,14 +14,15 @@ import com.blossom.backend.base.user.pojo.*;
import com.blossom.backend.config.BlConstants;
import com.blossom.backend.server.article.draft.pojo.ArticleStatRes;
import com.blossom.backend.server.article.stat.ArticleStatService;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.exception.XzException404;
import com.blossom.common.base.exception.XzException500;
import com.blossom.common.base.pojo.R;
import com.blossom.common.base.util.spring.SpringUtil;
import lombok.AllArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
@ -31,25 +34,86 @@ import java.util.Map;
@RequestMapping("/user")
@AllArgsConstructor
public class UserController {
private final UserService userService;
private final AuthService authService;
private final ArticleStatService articleService;
private final SysService sysService;
private final ParamService paramService;
private final UserParamService userParamService;
/**
* 获取用户列表
*
* @apiNote 需要管理员权限
* @since 1.13.0
*/
@AuthUserType(UserTypeEnum.ADMIN)
@GetMapping("/list")
public R<List<UserListRes>> list() {
return R.ok(userService.listAll(), UserListRes.class);
}
/**
* 用户信息
*
* @param id 用户ID
* @apiNote 根据用户ID获取用户信息, 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@GetMapping("/info/admin")
public R<BlossomUserRes> user(@RequestParam("id") Long id) {
return getUserById(id);
}
/**
* 新增用户
*
* @apiNote 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/add")
public R<?> add(@Validated @RequestBody UserAddReq req) {
userService.insert(req);
return R.ok();
}
/**
* 修改用户
*
* @apiNote 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/upd/admin")
public R<?> updateImportant(@Validated @RequestBody UserUpdAdminReq req) {
UserEntity user = req.to(UserEntity.class);
userService.updById(user);
return R.ok();
}
/**
* 禁用启用
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/disabled")
public R<?> disabled(@Validated @RequestBody UserDisabledReq req) {
if (req.getId().equals(AuthContext.getUserId())) {
throw new XzException500("不能禁用自己");
}
UserEntity user = req.to(UserEntity.class);
userService.updById(user);
authService.kickout(req.getId());
return R.ok();
}
/**
* 用户信息
*
* @apiNote 当前登录用户的用户信息
*/
@GetMapping("/info")
public R<BlossomUserRes> user() {
BlossomUserRes user = userService.selectById(AuthContext.getUserId()).to(BlossomUserRes.class);
user.setOsRes(sysService.getOsConfig());
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
user.setParams(paramMap);
paramMap.put("SERVER_VERSION", SpringUtil.get("project.base.version"));
Map<String, String> userParamMap = userParamService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
user.setUserParams(userParamMap);
return R.ok(user);
return getUserById(AuthContext.getUserId());
}
/**
@ -96,15 +160,33 @@ public class UserController {
}
/**
* 新增用户
* 删除用户
*/
@PostMapping("/add")
public R<?> add(@Validated @RequestBody UserAddReq req) {
UserEntity curUser = userService.getById(AuthContext.getUserId());
if (curUser == null || !UserTypeEnum.ADMIN.getType().equals(curUser.getType())) {
throw new XzException400("您没有权限添加用户");
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/del")
public R<?> delete(@Validated @RequestBody UserDeleteReq req) {
if (req.getId().equals(AuthContext.getUserId())) {
throw new XzException500("不能删除自己");
}
userService.insert(req);
userService.delete(req.getId());
authService.kickout(req.getId());
return R.ok();
}
/**
* 获取用户信息
*
* @param userId 用户ID
*/
private R<BlossomUserRes> getUserById(Long userId) {
BlossomUserRes user = userService.selectById(userId).to(BlossomUserRes.class);
user.setOsRes(sysService.getOsConfig());
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
user.setParams(paramMap);
paramMap.put("SERVER_VERSION", SpringUtil.get("project.base.version"));
Map<String, String> userParamMap = userParamService.selectMap(userId, true, UserParamEnum.values());
user.setUserParams(userParamMap);
return R.ok(user);
}
}

View File

@ -23,6 +23,13 @@ public interface UserMapper extends BaseMapper<UserEntity> {
*/
void updById(UserEntity user);
/**
* 删除用户
*
* @param userId 用户ID
*/
void delById(@Param("userId") Long userId);
/**
* 修改密码
*

View File

@ -1,15 +1,31 @@
package com.blossom.backend.base.user;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.base.auth.security.PasswordEncoder;
import com.blossom.backend.base.paramu.UserParamMapper;
import com.blossom.backend.base.paramu.UserParamService;
import com.blossom.backend.base.user.pojo.UserAddReq;
import com.blossom.backend.base.user.pojo.UserEntity;
import com.blossom.backend.base.user.pojo.UserUpdPwdReq;
import com.blossom.backend.server.article.draft.ArticleMapper;
import com.blossom.backend.server.article.log.ArticleLogMapper;
import com.blossom.backend.server.article.open.ArticleOpenMapper;
import com.blossom.backend.server.article.recycle.ArticleRecycleMapper;
import com.blossom.backend.server.article.reference.ArticleReferenceMapper;
import com.blossom.backend.server.article.stat.ArticleStatMapper;
import com.blossom.backend.server.article.view.ArticleViewMapper;
import com.blossom.backend.server.folder.FolderMapper;
import com.blossom.backend.server.note.NoteMapper;
import com.blossom.backend.server.picture.PictureMapper;
import com.blossom.backend.server.plan.PlanMapper;
import com.blossom.backend.server.todo.TodoMapper;
import com.blossom.backend.server.web.WebMapper;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.util.security.SaltUtil;
import com.blossom.common.iaas.OSManager;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -29,6 +45,21 @@ public class UserService extends ServiceImpl<UserMapper, UserEntity> {
private final PasswordEncoder passwordEncoder;
private final UserParamService userParamService;
private final UserParamMapper userParamMapper;
private final ArticleMapper articleMapper;
private final ArticleLogMapper articleLogMapper;
private final ArticleViewMapper articleViewMapper;
private final ArticleOpenMapper articleOpenMapper;
private final ArticleReferenceMapper articleReferenceMapper;
private final ArticleRecycleMapper articleRecycleMapper;
private final FolderMapper folderMapper;
private final NoteMapper noteMapper;
private final PlanMapper planMapper;
private final PictureMapper pictureMapper;
private final TodoMapper todoMapper;
private final WebMapper webMapper;
private final ArticleStatMapper statMapper;
private final OSManager osManager;
/**
* 查询全部用户
@ -79,6 +110,52 @@ public class UserService extends ServiceImpl<UserMapper, UserEntity> {
baseMapper.updById(user);
}
/**
* 修改用户信息
*/
@Transactional(rollbackFor = Exception.class)
public void delete(final Long userId) {
List<Long> articleIds = articleMapper.listIdByUserId(userId);
System.out.println(articleIds);
// base_user
baseMapper.delById(userId);
// base_user_param
userParamMapper.delByUserId(userId);
// blossom_article
articleMapper.delByUserId(userId);
// blossom_article_open
articleOpenMapper.delByUserId(userId);
// blossom_article_recycle
articleRecycleMapper.delByUserId(userId);
// blossom_article_reference
articleReferenceMapper.delByUserId(userId);
if (CollUtil.isNotEmpty(articleIds)) {
// blossom_article_log
articleLogMapper.delByIds(articleIds);
// blossom_article_view
articleViewMapper.delByIds(articleIds);
}
// blossom_folder
folderMapper.delByUserId(userId);
// blossom_note
noteMapper.delByUserId(userId);
// blossom_picture
pictureMapper.delByUserId(userId);
// blossom_plan
planMapper.delByUserId(userId);
// blossom_stat
statMapper.delByUserId(userId);
// blossom_todo
todoMapper.delByUserId(userId);
// blossom_web
webMapper.delByUserId(userId);
// 删除物理文件
final String rootPath = osManager.getDefaultPath();
final String uid = "/U" + userId;
osManager.deletePath(rootPath + uid);
}
/**
* 修改密码
*/

View File

@ -7,6 +7,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
/**
@ -56,6 +57,14 @@ public class BlossomUserRes extends AbstractPOJO implements Serializable {
* 文章字数
*/
private Integer articleWords;
/**
* 创建日期
*/
private Date creTime;
/**
* 逻辑删除, 目前用于禁用用户, 而不是删除
*/
private Long delTime;
/**
* 对象存储信息, 非登录状态不返回该字段

View File

@ -0,0 +1,17 @@
package com.blossom.backend.base.user.pojo;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 删除用户
*/
@Data
public class UserDeleteReq {
/**
* 用户ID
*/
@NotNull(message = "用户ID为必填项")
private Long id;
}

View File

@ -0,0 +1,20 @@
package com.blossom.backend.base.user.pojo;
import com.blossom.common.base.pojo.AbstractPOJO;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
public class UserDisabledReq extends AbstractPOJO {
/**
* 用户ID
*/
@NotNull(message = "用户ID为必填项")
private Long id;
/**
* 禁用状态
*/
private Long delTime;
}

View File

@ -8,6 +8,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* 用户
@ -66,5 +67,13 @@ public class UserEntity extends AbstractPOJO implements Serializable {
* 用户的位置
*/
private String location;
/**
* 创建日期
*/
private Date creTime;
/**
* 逻辑删除, 目前用于禁用用户, 而不是删除
*/
private Long delTime;
}

View File

@ -0,0 +1,37 @@
package com.blossom.backend.base.user.pojo;
import lombok.Data;
import java.util.Date;
@Data
public class UserListRes {
/**
* 用户ID
*/
private Long id;
/**
* 用户头像
*/
private String avatar;
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickName;
/**
* 用户类型
*/
private Integer type;
/**
* 创建日期
*/
private Date creTime;
/**
* 逻辑删除, 目前用于禁用用户, 而不是删除
*/
private Long delTime;
}

View File

@ -0,0 +1,27 @@
package com.blossom.backend.base.user.pojo;
import com.blossom.common.base.pojo.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户请求
*
* @author xzzz
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserUpdAdminReq extends AbstractEntity {
/**
* 用户ID
*/
private Long id;
/**
* 和风天气的位置
*/
private String location;
/**
* 类型
*/
private Integer type;
}

View File

@ -37,6 +37,5 @@ public class UserUpdReq extends AbstractEntity {
/**
* 用户头像
*/
@NotBlank(message = "用户头像为必填项")
private String avatar;
}

View File

@ -15,6 +15,7 @@ import javax.sql.DataSource;
import java.net.InetSocketAddress;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.TimeZone;
/**
* 启动检查配置文件内容, 用于检查配置项是否正确
@ -34,6 +35,7 @@ public class PropertiesCheckListener implements ApplicationListener<ApplicationE
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment env = event.getEnvironment();
setTimeZone(env);
log.warn("\n\n正在检查 Blossom 后台配置项\n\n" +
"\n[CHECK] ==========================================================================================================================" +
"\n[CHECK] 使用环境: [{}], 版本: [{}]" +
@ -46,6 +48,7 @@ public class PropertiesCheckListener implements ApplicationListener<ApplicationE
"\n[CHECK] 文件大小: {}" +
"\n[CHECK] 授权时长: {}" +
"\n[CHECK] 重置密码: {}" +
"\n[CHECK] 指定时区: {}" +
"\n[CHECK] ==========================================================================================================================\n\n",
get(env, SpringUtil.PROFILE_ACTION), get(env, "project.base.version"),
get(env, "spring.datasource.url"),
@ -55,7 +58,8 @@ public class PropertiesCheckListener implements ApplicationListener<ApplicationE
get(env, "project.iaas.blos.default-path"),
get(env, "spring.servlet.multipart.max-file-size"),
get(env, "project.auth.clients[0].duration"),
get(env, "project.auth.password-reset")
get(env, "project.auth.password-reset"),
get(env, "project.base.time-zone")
);
String defaultPath = get(env, "project.iaas.blos.default-path");
@ -167,4 +171,15 @@ public class PropertiesCheckListener implements ApplicationListener<ApplicationE
public boolean isNotReceived(String message) {
return message.contains("The driver has not received any packets from the server");
}
private void setTimeZone(ConfigurableEnvironment env) {
String timeZone = get(env, "project.base.time-zone");
if (StrUtil.isBlank(timeZone) || TimeZone.getTimeZone(timeZone) == null) {
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
} else {
TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
}
}
}

View File

@ -289,13 +289,19 @@ public class ArticleController {
/**
* 创建文章的临时访问缓存
*
* @param id 文章ID
* @param id 文章ID
* @param duration 临时访问的过期时间
* @return 临时访问Key
* @since 1.9.0
*/
@GetMapping("/temp/key")
public R<String> createTempVisitKey(@RequestParam("id") Long id) {
return R.ok(tempVisitService.create(id, AuthContext.getUserId()));
public R<String> createTempVisitKey(@RequestParam("id") Long id,
@RequestParam(value = "duration", required = false) Long duration) {
if (duration == null) {
duration = 3 * 60L;
}
log.info("创建文章临时访问权限 [{}:{}m]", id, duration);
return R.ok(tempVisitService.create(id, AuthContext.getUserId(), duration));
}
/**

View File

@ -29,6 +29,12 @@ public interface ArticleMapper extends BaseMapper<ArticleEntity> {
*/
List<ArticleEntity> listAll(ArticleEntity entity);
/**
* 获取指定用户的全部文章ID
* @param userId 用户ID
*/
List<Long> listIdByUserId(Long userId);
/**
* 查询全部需要索引的字段
*/
@ -70,4 +76,8 @@ public interface ArticleMapper extends BaseMapper<ArticleEntity> {
*/
void uvAndPv(@Param("articleId") Long article, @Param("pv") Integer pv, @Param("uv") Integer uv);
/**
* 删除文章
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -3,6 +3,7 @@ package com.blossom.backend.server.article.draft;
import cn.hutool.core.util.ObjUtil;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.util.security.SHA256Util;
import com.blossom.common.cache.caffeine.DynamicExpiry;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
@ -10,6 +11,7 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -24,25 +26,31 @@ public class ArticleTempVisitService {
/**
* 存放文章ID的缓存
*
* @since 1.13.0 支持动态过期时间
*/
private final Cache<String, TempVisit> tempVisitCache = Caffeine.newBuilder()
.expireAfter(new DynamicExpiry())
.initialCapacity(500)
.expireAfterWrite(3, TimeUnit.HOURS)
.removalListener((String key, TempVisit value, RemovalCause cause) ->
log.info("临时访问文章 [" + value.getArticleId() + "] 被删除")
log.info("remove temp visit articleId [" + Objects.requireNonNull(value).getArticleId() + "]")
)
.build();
/**
* 生成一个缓存 key, key 并非文章的摘要码
* 生成一个缓存 key, key 并非文章的摘要码,
*
* @param articleId 文章ID
* @param userId 用户ID
* @param duration 过期时间, 单位分钟
* @return 缓存 key
*/
public String create(Long articleId, Long userId) {
public String create(Long articleId, Long userId, Long duration) {
XzException400.throwBy(ObjUtil.isNull(articleId), "文章ID为必填项");
String key = SHA256Util.encode(UUID.randomUUID().toString());
tempVisitCache.put(key, new TempVisit(articleId, userId));
tempVisitCache.policy().expireVariably().ifPresent(e -> {
e.put(key, new TempVisit(articleId, userId), duration, TimeUnit.MINUTES);
});
return key;
}

View File

@ -20,4 +20,11 @@ public interface ArticleLogMapper extends BaseMapper<ArticleLogEntity> {
* @param articleId 文章ID
*/
List<ArticleLogEntity> listAll(@Param("articleId") Long articleId);
/**
* 删除文章的记录
*
* @param articleIds 文章ID集合
*/
Long delByIds(@Param("articleIds") List<Long> articleIds);
}

View File

@ -3,6 +3,7 @@ package com.blossom.backend.server.article.open;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 公开文章
@ -32,4 +33,9 @@ public interface ArticleOpenMapper extends BaseMapper<ArticleOpenEntity> {
* @param id 公开文章的ID
*/
void sync(Long id);
/**
* 删除公开文章
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -37,4 +37,9 @@ public interface ArticleRecycleMapper extends BaseMapper<ArticleRecycleEntity> {
* @param pid 文章的父ID
*/
void restore(@Param("id") Long id, @Param("pid") Long pid);
/**
* 删除文章回收站
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -61,4 +61,8 @@ public interface ArticleReferenceMapper extends BaseMapper<ArticleReferenceEntit
*/
void updateToKnown(@Param("userId") Long userId, @Param("targetId") Long targetId, @Param("targetName") String targetName);
/**
* 删除文章引用
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.server.article.reference.pojo.ArticleReferenceEntity;
import com.blossom.backend.server.article.reference.pojo.ArticleReferenceReq;
import com.blossom.common.base.util.BeanUtil;
import com.blossom.common.base.util.security.Base64Util;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
@ -48,6 +49,9 @@ public class ArticleReferenceService extends ServiceImpl<ArticleReferenceMapper,
ref.setUserId(userId);
ref.setSourceId(sourceId);
ref.setSourceName(sourceName);
if (Base64Util.isBase64Img(ref.getTargetUrl())) {
ref.setTargetUrl("");
}
}
baseMapper.insertList(refs);
}

View File

@ -2,12 +2,16 @@ package com.blossom.backend.server.article.stat;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.auth.exception.AuthException;
import com.blossom.backend.base.auth.exception.AuthRCode;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.backend.config.BlConstants;
import com.blossom.backend.server.article.draft.pojo.ArticleStatRes;
import com.blossom.backend.server.article.stat.pojo.ArticleHeatmapRes;
import com.blossom.backend.server.article.stat.pojo.ArticleLineRes;
import com.blossom.backend.server.article.stat.pojo.ArticleWordsSaveReq;
import com.blossom.backend.server.article.stat.pojo.ArticleWordsRes;
import com.blossom.backend.server.article.stat.pojo.ArticleWordsSaveReq;
import com.blossom.common.base.pojo.R;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -77,6 +81,21 @@ public class ArticleStatController {
return R.ok(statService.statCount(null, null, AuthContext.getUserId()));
}
/**
* 文章数和文章字数
*
* @apiNote 只有管理员可以查看
* @since 1.13.0
*/
@AuthUserType(UserTypeEnum.ADMIN)
@GetMapping("/words/user")
public R<ArticleStatRes> word(@RequestParam("id") Long id) {
if (!AuthContext.getType().equals(UserTypeEnum.ADMIN.getType())) {
throw new AuthException(AuthRCode.PERMISSION_DENIED);
}
return R.ok(statService.statCount(null, null, id));
}
/**
* 字数统计列表
*

View File

@ -3,6 +3,7 @@ package com.blossom.backend.server.article.stat;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.blossom.backend.server.article.stat.pojo.ArticleStatEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 文章统计
@ -11,4 +12,9 @@ import org.apache.ibatis.annotations.Mapper;
*/
@Mapper
public interface ArticleStatMapper extends BaseMapper<ArticleStatEntity> {
/**
* 删除统计信息
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -5,6 +5,8 @@ import com.blossom.backend.server.article.view.pojo.ArticleViewEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 文章访问记录 [@A#View]
@ -22,4 +24,11 @@ public interface ArticleViewMapper extends BaseMapper<ArticleViewEntity> {
* @param articleId 文章ID
*/
Long checkUv(@Param("today") String today, @Param("ip") String ip, @Param("articleId") Long articleId);
/**
* 删除文章的访问记录
*
* @param articleIds 文章ID集合
*/
void delByIds(@Param("articleIds") List<Long> articleIds);
}

View File

@ -39,4 +39,9 @@ public interface FolderMapper extends BaseMapper<FolderEntity> {
* 根据ID集合修改
*/
void updByIds(FolderEntity entity);
/**
* 删除文件夹
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -28,4 +28,9 @@ public interface NoteMapper extends BaseMapper<NoteEntity> {
* @param userId 用户ID
*/
void updById(@Param("id") Long id, @Param("userId") Long userId, @Param("content") String content);
/**
* 删除便签
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -2,6 +2,8 @@ package com.blossom.backend.server.picture;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.backend.server.picture.pojo.*;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.pojo.PageRes;
@ -116,4 +118,15 @@ public class PictureController {
public R<PictureStatRes> stat(@RequestParam(value = "pid", required = false) Long pid) {
return R.ok(baseService.stat(AuthContext.getUserId(), pid));
}
/**
* 查询用户的图片统计
*
* @param userId 用户ID
*/
@AuthUserType(UserTypeEnum.ADMIN)
@GetMapping("/stat/user")
public R<PictureStatRes> statUser(@RequestParam(value = "id") Long userId) {
return R.ok(baseService.stat(userId, null));
}
}

View File

@ -56,4 +56,9 @@ public interface PictureMapper extends BaseMapper<PictureEntity> {
* @param pid 文件夹ID
*/
PictureStatRes stat(@Param("userId") Long userId, @Param("pid") Long pid);
/**
* 删除图片
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -8,7 +8,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -32,7 +31,7 @@ public class PlanController {
* @param month 查询的月份, 会查询该月的所有每日计划
*/
@GetMapping("/list/day")
public R<Map<Date, List<PlanDayRes>>> days(String month) {
public R<Map<String, List<PlanDayRes>>> days(String month) {
return R.ok(baseService.listDay(month, AuthContext.getUserId()));
}

View File

@ -40,4 +40,9 @@ public interface PlanMapper extends BaseMapper<PlanEntity> {
* @param groupId 计划分组ID
*/
void delById(@Param("id") Long id, @Param("groupId") Long groupId);
/**
* 删除计划
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -28,12 +28,19 @@ public class PlanService extends ServiceImpl<PlanMapper, PlanEntity> {
/**
* 按月查询每日计划
*/
public Map<Date, List<PlanDayRes>> listDay(String month, Long userId) {
public Map<String, List<PlanDayRes>> listDay(String month, Long userId) {
PlanEntity where = new PlanEntity();
where.setPlanMonth(month);
where.setUserId(userId);
where.setType(PlanTypeEnum.DAY.getType());
return PlanUtil.sortToTreeMap(BeanUtil.toList(baseMapper.listAll(where), PlanDayRes.class), false);
List<PlanDayRes> list = new ArrayList<>();
for (PlanEntity plan : baseMapper.listAll(where)) {
PlanDayRes res = BeanUtil.toObj(plan, PlanDayRes.class);
res.setPlanDate(DateUtils.toYMD(plan.getPlanDate()) + " 00:00:00");
list.add(res);
}
return PlanUtil.sortToTreeMap(list, false);
}
/**

View File

@ -3,7 +3,6 @@ package com.blossom.backend.server.plan.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 每日计划
@ -34,7 +33,7 @@ public class PlanDayRes implements Serializable {
/**
* 日期
*/
private Date planDate;
private String planDate;
/**
* 计划开始时间
*/

View File

@ -90,4 +90,9 @@ public interface TodoMapper extends BaseMapper<TodoEntity> {
* 根据ID集合删除
*/
void delByIds(@Param("ids") List<Long> ids, @Param("userId") Long userId);
/**
* 删除待办
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -4,10 +4,12 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.blossom.backend.server.plan.pojo.PlanDayRes;
import com.blossom.common.base.util.DateUtils;
import com.blossom.common.base.util.SortUtil;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
@ -22,8 +24,8 @@ public class PlanUtil {
*
* @param plans 计划列表
*/
private static TreeMap<Date, List<PlanDayRes>> byDay(List<PlanDayRes> plans) {
TreeMap<Date, List<PlanDayRes>> map = new TreeMap<>();
private static TreeMap<String, List<PlanDayRes>> byDay(List<PlanDayRes> plans) {
TreeMap<String, List<PlanDayRes>> map = new TreeMap<>();
for (PlanDayRes plan : plans) {
List<PlanDayRes> list = map.getOrDefault(plan.getPlanDate(), new ArrayList<>());
list.add(plan);
@ -57,7 +59,7 @@ public class PlanUtil {
if (CollUtil.isEmpty(plans)) {
return plans;
}
Map<Date, List<PlanDayRes>> byDay = byDay(plans);
Map<String, List<PlanDayRes>> byDay = byDay(plans);
Map<Long, List<PlanDayRes>> byGroupId = byGroupId(plans);
// 先为所有计划设置默认排序
@ -105,13 +107,13 @@ public class PlanUtil {
* @param plans 计划列表, 传入的计划列表需要提前按 {@link PlanDayRes#getPlanDate()} 升序
* @return treeMap key 为日期(), value 为该日期计划数组
*/
public static TreeMap<Date, List<PlanDayRes>> sortToTreeMap(List<PlanDayRes> plans, boolean debug) {
public static TreeMap<String, List<PlanDayRes>> sortToTreeMap(List<PlanDayRes> plans, boolean debug) {
if (CollUtil.isEmpty(plans)) {
return new TreeMap<>();
}
Map<Date, List<PlanDayRes>> map = sort(plans).stream()
Map<String, List<PlanDayRes>> map = sort(plans).stream()
.collect(Collectors.groupingBy(PlanDayRes::getPlanDate));
TreeMap<Date, List<PlanDayRes>> treeMap = MapUtil.sort(map);
TreeMap<String, List<PlanDayRes>> treeMap = MapUtil.sort(map);
treeMap.forEach((k, v) -> {
v = v.stream().sorted((p1, p2) -> SortUtil.intSort.compare(p1.getSort(), p2.getSort())).collect(Collectors.toList());
treeMap.put(k, v);
@ -127,9 +129,9 @@ public class PlanUtil {
/**
* 控制台打印计划的排序
*/
private static void debugTreeMap(TreeMap<Date, List<PlanDayRes>> treeMap) {
for (Date date : treeMap.keySet()) {
System.out.print(DateUtils.format(date, DateUtils.PATTERN_YYYYMMDD) + "|");
private static void debugTreeMap(TreeMap<String, List<PlanDayRes>> treeMap) {
for (String date : treeMap.keySet()) {
System.out.print(date + "|");
}
System.out.println();
for (int i = 0; i < treeMap.values().size(); i++) {
@ -151,7 +153,7 @@ public class PlanUtil {
* @param date 日期
* @param sort 顺序
*/
private static PlanDayRes getHolderPlan(Date date, int sort) {
private static PlanDayRes getHolderPlan(String date, int sort) {
PlanDayRes holder = new PlanDayRes();
holder.setId(sort * -1L - 1);
holder.setGroupId(0L);
@ -166,31 +168,31 @@ public class PlanUtil {
PlanDayRes p1_1 = new PlanDayRes();
p1_1.setId(1L);
p1_1.setGroupId(1L);
p1_1.setPlanDate(DateUtils.parse("2023-09-01", DateUtils.PATTERN_YYYYMMDD));
p1_1.setPlanDate("2023-09-01");
plans.add(p1_1);
PlanDayRes p1_2 = new PlanDayRes();
p1_2.setId(2L);
p1_2.setGroupId(1L);
p1_2.setPlanDate(DateUtils.parse("2023-09-02", DateUtils.PATTERN_YYYYMMDD));
p1_2.setPlanDate("2023-09-02");
plans.add(p1_2);
PlanDayRes p2 = new PlanDayRes();
p2.setId(3L);
p2.setGroupId(2L);
p2.setPlanDate(DateUtils.parse("2023-09-01", DateUtils.PATTERN_YYYYMMDD));
p2.setPlanDate("2023-09-01");
plans.add(p2);
PlanDayRes p3_1 = new PlanDayRes();
p3_1.setId(4L);
p3_1.setGroupId(3L);
p3_1.setPlanDate(DateUtils.parse("2023-09-02", DateUtils.PATTERN_YYYYMMDD));
p3_1.setPlanDate("2023-09-02");
plans.add(p3_1);
PlanDayRes p3_2 = new PlanDayRes();
p3_2.setId(4L);
p3_2.setGroupId(3L);
p3_2.setPlanDate(DateUtils.parse("2023-09-03", DateUtils.PATTERN_YYYYMMDD));
p3_2.setPlanDate("2023-09-03");
plans.add(p3_2);
sortToTreeMap(plans, true);

View File

@ -3,6 +3,7 @@ package com.blossom.backend.server.web;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.blossom.backend.server.web.pojo.WebEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 网站收藏
@ -12,4 +13,8 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface WebMapper extends BaseMapper<WebEntity> {
/**
* 删除文章
*/
void delByUserId(@Param("userId") Long userId);
}

View File

@ -1,14 +1,13 @@
package com.blossom.backend.thirdparty;
import cn.hutool.core.util.StrUtil;
import com.blossom.backend.base.user.UserService;
import com.blossom.backend.base.user.pojo.UserEntity;
import com.blossom.backend.thirdparty.hefeng.WeatherManager;
import com.blossom.common.base.pojo.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.blossom.expand.tracker.core.adapter.aspect.TrackerStart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -22,11 +21,9 @@ import java.util.stream.Collectors;
*
* @author : xzzz
*/
@Component
@RestController
@RequestMapping("/thirdparty/scheduled")
public class ThirdPartyScheduled {
private static final Logger log = LoggerFactory.getLogger(ThirdPartyScheduled.class);
@Autowired
private WeatherManager weatherManager;
@ -39,14 +36,17 @@ public class ThirdPartyScheduled {
*
* @apiNote 每30分钟刷新, 请求会立即刷新
*/
@TrackerStart
@PostMapping("/weather")
@Scheduled(cron = "0 0/30 * * * ?")
public R<?> refreshWeather() {
log.debug("[BLOSSOM] 刷新天气");
List<UserEntity> users = userService.listAll();
Set<String> locations = users.stream().collect(Collectors.groupingBy(UserEntity::getLocation)).keySet();
for (String location : locations) {
weatherManager.clearAll(location);
if (StrUtil.isBlank(location)) {
continue;
}
weatherManager.clear(location);
weatherManager.findWeatherAll(location);
}
return R.ok();

View File

@ -23,9 +23,11 @@ public class WeatherController {
/**
* 获取天气信息
*
* @param location 用户位置
*/
@GetMapping
public R<WeatherRes> weather(@RequestParam(value = "location",required = false)String location) {
public R<WeatherRes> weather(@RequestParam(value = "location", required = false) String location) {
if (StrUtil.isBlank(location)) {
return R.ok(new WeatherRes());
}

View File

@ -9,17 +9,19 @@ import com.blossom.backend.thirdparty.hefeng.pojo.*;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.util.json.JsonUtil;
import com.blossom.common.base.util.okhttp.HttpUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 天气查询
@ -38,19 +40,32 @@ public class WeatherManager {
private static final String URL_NOW = "https://devapi.heweather.net/v7/weather/now";
private static final String URL_DAILY = "https://devapi.heweather.net/v7/weather/3d";
private static final String URL_HOURLY = "https://devapi.heweather.net/v7/weather/24h";
private static final String WEATHER_ALL = "weather_all";
private static final String SUCCESS = "200";
/**
*
*/
private final Cache<String, WeatherRes> weatherCache = Caffeine.newBuilder()
.initialCapacity(50)
.expireAfterWrite(45, TimeUnit.MINUTES)
.removalListener((String location, WeatherRes weather, RemovalCause cause) ->
log.info("Weather cache [" + location + "] has been deleted")
)
.build();
@Autowired
private ParamService paramService;
/**
* 查询天气信息
*/
@Cacheable(cacheNames = WEATHER_ALL, key = "'location_' + #location", unless = "#result == null")
public WeatherRes findWeatherAll(String location) {
WeatherRes cache = weatherCache.getIfPresent(location);
if (cache != null) {
log.debug("[BLOSSOM] get weather from cache: {}", location);
return cache;
}
log.info("[BLOSSOM] refresh weather: {}", location);
Map<String, String> maps = initParam(location);
if (maps == null) {
log.info("未配置天气信息, 忽略天气查询");
@ -128,15 +143,15 @@ public class WeatherManager {
} else {
log.error("获取小时预报失败, resp: {}", cityStr);
}
weatherCache.put(location, weather);
return weather;
}
/**
* 清除缓存
*/
@CacheEvict(cacheNames = WEATHER_ALL, key = "'location_' + #location")
public void clearAll(String location) {
public void clear(String location) {
weatherCache.invalidate(location);
}
/**

View File

@ -1,7 +1,7 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.31.99:3306/xzzz-blossom?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8
url: jdbc:mysql://localhost:3306/blossom?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8
username: root
password: jasmine888
hikari:
@ -29,6 +29,7 @@ logging:
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ project ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
project:
base:
time-zone: GMT+8
version: @project.version@
ex:
# 异常打印格式 all/project
@ -58,7 +59,6 @@ project:
duration: 21600 # 客户端授权时间 6 小时
multi-place-login: true # 客户端是否允许多地登录
white-list: # 白名单
- /sentinel/**
- /editor/**
- /blog/**
- /assets/**

View File

@ -20,6 +20,7 @@ logging:
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ project ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
project:
base:
time-zone: GMT+8
version: @project.version@
ex:
# 异常打印格式 all/project

View File

@ -23,6 +23,7 @@ logging:
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ project ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
project:
base:
time-zone: GMT+8
version: @project.version@
ex:
# 异常打印格式 all/project

View File

@ -8,4 +8,9 @@
where article_id = #{articleId}
order by cre_time desc
</select>
<delete id="delByIds">
delete from blossom_article_log
where article_id in <foreach collection="articleIds" item="item" open="(" close=")" separator=",">#{item}</foreach>
</delete>
</mapper>

View File

@ -48,6 +48,10 @@
</where>
</select>
<select id="listIdByUserId" resultType="java.lang.Long">
select id from blossom_article where user_id = #{userId}
</select>
<!-- 查询全部文章,包含id, name , markdown, tags , userId字段用于批量索引的建立 -->
<select id="listAllIndexField" resultType="com.blossom.backend.server.article.draft.pojo.ArticleEntity">
select
@ -121,4 +125,9 @@
</if>
where id = #{articleId}
</update>
<delete id="delByUserId">
delete from blossom_article where user_id = #{userId}
</delete>
</mapper>

View File

@ -27,4 +27,8 @@
o.html = a.html,
o.sync_time = now()
</update>
<delete id="delByUserId">
delete from blossom_article_open where user_id = #{userId}
</delete>
</mapper>

View File

@ -65,4 +65,8 @@
from blossom_article_recycle
where id = #{id})
</insert>
<delete id="delByUserId">
delete from blossom_article_recycle where user_id = #{userId}
</delete>
</mapper>

View File

@ -87,4 +87,8 @@
where target_Id = #{targetId}
and user_id = #{userId}
</update>
<delete id="delByUserId">
delete from blossom_article_reference where user_id = #{userId}
</delete>
</mapper>

View File

@ -9,4 +9,8 @@
and cre_day = #{today}
</select>
<delete id="delByIds">
delete from blossom_article_view
where article_id in <foreach collection="articleIds" item="item" open="(" close=")" separator=",">#{item}</foreach>
</delete>
</mapper>

View File

@ -83,4 +83,9 @@
</set>
where id in <foreach collection="ids" item="item" open="(" close=")" separator=",">#{item}</foreach>
</update>
<delete id="delByUserId">
delete from blossom_folder where user_id = #{userId}
</delete>
</mapper>

View File

@ -22,4 +22,9 @@
where id = #{id}
and user_id = #{userId}
</update>
<delete id="delByUserId">
delete from blossom_note where user_id = #{userId}
</delete>
</mapper>

View File

@ -71,5 +71,7 @@
<if test="pid != null">and pid = #{pid}</if>
</select>
<delete id="delByUserId">
delete from blossom_picture where user_id = #{userId}
</delete>
</mapper>

View File

@ -78,4 +78,8 @@
</if>
</delete>
<delete id="delByUserId">
delete from blossom_plan where user_id = #{userId}
</delete>
</mapper>

View File

@ -0,0 +1,9 @@
<?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="com.blossom.backend.server.article.stat.ArticleStatMapper">
<delete id="delByUserId">
delete from blossom_stat where user_id = #{userId}
</delete>
</mapper>

View File

@ -136,4 +136,8 @@
<foreach collection="ids" item="item" open="(" close=")" separator=",">#{item}</foreach>
</delete>
<delete id="delByUserId">
delete from blossom_todo where user_id = #{userId}
</delete>
</mapper>

View File

@ -24,6 +24,8 @@
<if test="avatar != null">avatar = #{avatar},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="location != null">location = #{location},</if>
<if test="delTime != null">del_time = #{delTime},</if>
<if test="type != null">type = #{type},</if>
upd_time = now()
</set>
where id = #{id}
@ -33,4 +35,7 @@
update base_user set password = '${password}',upd_time = now() where id = #{userId}
</update>
<delete id="delById">
delete from base_user where id = #{userId}
</delete>
</mapper>

View File

@ -26,4 +26,8 @@
<insert id="insertByUserId">
insert into base_user_param values (null, #{userId}, #{paramName}, #{paramValue}, now())
</insert>
<delete id="delByUserId">
delete from base_user_param where user_id = #{userId}
</delete>
</mapper>

View File

@ -0,0 +1,9 @@
<?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="com.blossom.backend.server.web.WebMapper">
<delete id="delByUserId">
delete from blossom_web where user_id = #{userId}
</delete>
</mapper>

View File

@ -387,7 +387,7 @@ CREATE TABLE IF NOT EXISTS `blossom_article_reference`
) ENGINE = InnoDB
AUTO_INCREMENT = 100000
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_bin
COLLATE = utf8mb4_bin COMMENT = '文章引用'
ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -478,7 +478,7 @@ CREATE TABLE IF NOT EXISTS `blossom_note`
) ENGINE = InnoDB
AUTO_INCREMENT = 18
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_bin
COLLATE = utf8mb4_bin COMMENT = '便签Note'
ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -559,7 +559,7 @@ CREATE TABLE IF NOT EXISTS `blossom_stat`
) ENGINE = InnoDB
AUTO_INCREMENT = 218
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_bin
COLLATE = utf8mb4_bin COMMENT = '统计信息'
ROW_FORMAT = DYNAMIC;
-- ----------------------------
@ -584,7 +584,7 @@ CREATE TABLE IF NOT EXISTS `blossom_web`
) ENGINE = InnoDB
AUTO_INCREMENT = 292
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_bin COMMENT = '[FS] 网站收藏'
COLLATE = utf8mb4_bin COMMENT = '网站收藏'
ROW_FORMAT = DYNAMIC;
alter table blossom_web
@ -637,5 +637,5 @@ CREATE TABLE IF NOT EXISTS `base_user_param`
PRIMARY KEY (`id`),
UNIQUE KEY `unq_bup_userid_paramname` (`user_id`, `param_name`) COMMENT '用户参数唯一'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
DEFAULT CHARSET = utf8mb4 COMMENT = '用户参数'
COLLATE = utf8mb4_bin;

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>

View File

@ -1,5 +1,6 @@
package com.blossom.common.base;
import com.blossom.common.base.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
@ -22,7 +23,7 @@ public final class BaseConstants {
public static void desc() {
log.info("启动完成" +
"\n=========================================================================" +
"\n启动成功: 可使用客户端登录, 默认用户名/密码: blos/blos" +
"\n启动成功 [" + DateUtils.now() + "], 可使用客户端登录, 默认用户名/密码: blos/blos" +
"\n下载地址: https://github.com/blossom-editor/blossom/releases" +
"\n文档地址: https://www.wangyunf.com/blossom-doc/index" +
"\n博客端访问地址: http://IP:端口(域名)/blog/#/home" +

View File

@ -17,6 +17,11 @@ import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "project.base")
public class BaseProperties {
/**
* 指定项目的时区
*/
private String timeZone = "GMT+8";
/**
* 系统版本, 可以使用 @project.version@ 获取 pom 中版本
*/

View File

@ -1,20 +1,34 @@
package com.blossom.common.base.util.security;
import java.io.IOException;
import cn.hutool.core.util.StrUtil;
import java.util.Base64;
/**
* 消息编码算法
*
* @author xzzz
* @since 0.0.1
*/
public class Base64Util {
public static String encrypt(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
public static String encrypt(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
public static String decrypt(String data) throws IOException {
return new String(Base64.getDecoder().decode(data));
}
public static String decrypt(String data) {
return new String(Base64.getDecoder().decode(data));
}
/**
* 是否 base64 图片, 只校验格式, 不判断内容是否为正确的图片
* 例如: 传入 data:image/png;base64,a, 将会返回 true
*/
public static boolean isBase64Img(String image) {
if (StrUtil.isBlank(image)) {
return false;
}
String prefix = image.substring(0, Math.max(image.indexOf(','), 0));
return prefix.startsWith("data:image") && prefix.endsWith("base64");
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -0,0 +1,28 @@
package com.blossom.common.cache.caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* key 使用不同的过期时间
*
* @since 1.13.0
*/
public class DynamicExpiry implements Expiry<String, Object> {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull Object value, long currentTime) {
return 0;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
}

View File

@ -1,4 +1,4 @@
package com.blossom.common.base.caffeine;
package com.blossom.common.cache.caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
@ -13,25 +13,31 @@ import java.util.concurrent.TimeUnit;
public class Test {
private static final Cache<String, String> cache = Caffeine.newBuilder()
.expireAfter(new DynamicExpiry())
.initialCapacity(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.removalListener((String key, String value, RemovalCause cause) -> System.out.println(key + " 被删除"))
.removalListener((String key, String value, RemovalCause cause) -> log.info(key + " 被删除"))
.build();
private static final ScheduledExecutorService clearUpScheduled = Executors.newScheduledThreadPool(1);
public Test() {
clearUpScheduled.scheduleWithFixedDelay(this::clear, 5, 1, TimeUnit.SECONDS);
clearUpScheduled.scheduleWithFixedDelay(this::clear, 0, 1, TimeUnit.SECONDS);
}
private void clear() {
log.info("尝试过期缓存");
log.info("删除");
cache.cleanUp();
}
public static void main(String[] args) {
Test test = new Test();
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
cache.policy().expireVariably().ifPresent(e -> {
e.put("A1", "1", 7, TimeUnit.SECONDS);
});
cache.policy().expireVariably().ifPresent(e -> {
e.put("A2", "2", 3, TimeUnit.SECONDS);
});
Thread.sleep(20 * 1000);
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -79,4 +79,8 @@ public abstract class AbstractOSManager implements OSManager {
}
}
@Override
public boolean deletePath(String pathname) {
return false;
}
}

View File

@ -73,4 +73,11 @@ public interface OSManager {
* @return 是否删除
*/
boolean delete(String filename);
/**
* 删除文件夹
*
* @param pathname 文件夹路径
*/
boolean deletePath(String pathname);
}

View File

@ -111,4 +111,9 @@ public class BLOSManager extends AbstractOSManager implements OSManager {
File file = FileUtil.newFile(filename);
return file.delete();
}
@Override
public boolean deletePath(String pathname) {
return FileUtil.del(pathname);
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.blossom</groupId>
<artifactId>blossom-backend</artifactId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.blossom</groupId>
<artifactId>expand-sentinel</artifactId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -191,7 +191,7 @@ public class SentinelMetricController extends AbstractSentinelController {
String interval,
Integer customInterval,
TimeUnit customIntervalUnit) {
log.info("查询 Sentinel 单机流量折线图");
log.debug("[SENTINEL] 查询 Sentinel 单机流量折线图");
ResourceHistogram rh = explain(startTime, endTime, interval, customInterval, customIntervalUnit, "local");
// 读取日志
String all = this.metric(resource, rh.getStartTime(), rh.getEndTime(), "");

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.blossom</groupId>
<artifactId>expand-sentinel</artifactId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>blossom-backend</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>expand-tracker</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>blossom-backend</artifactId>
<groupId>com.blossom</groupId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -14,11 +14,11 @@
<groupId>com.blossom</groupId>
<artifactId>blossom-backend</artifactId>
<version>1.12.0-SNAPSHOT</version>
<version>1.13.0-SNAPSHOT</version>
<!-- 版本控制 -->
<properties>
<revision>1.12.0-SNAPSHOT</revision>
<revision>1.13.0-SNAPSHOT</revision>
<!-- 编译设置 -->
<java.version>1.8</java.version>

View File

@ -1,12 +1,12 @@
{
"name": "blossom",
"version": "1.12.0",
"version": "1.13.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "blossom",
"version": "1.12.0",
"version": "1.13.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@ -1,7 +1,7 @@
{
"name": "blossom",
"productName": "Blossom",
"version": "1.12.0",
"version": "1.13.0",
"description": "A markdown editor",
"license": "MIT",
"main": "./out/main/index.js",

View File

@ -387,7 +387,7 @@
<br />
<span style="font-size: 13px">加载中...</span>
</div>
<div class="html-loading-version">Blossom | v1.12.0</div>
<div class="html-loading-version">Blossom | v1.13.0</div>
</div>
</div>
</div>

View File

@ -1,9 +1,6 @@
<template>
<el-config-provider :size="'small'" :locale="zhCn">
<div class="app">
<div class="app-header">
<AppHeader></AppHeader>
</div>
<div class="app-main">
<RouterView />
</div>
@ -13,7 +10,6 @@
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import AppHeader from './components/AppHeader.vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>
@ -21,12 +17,9 @@ import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
.app {
@include box(100%, 100%);
overflow: hidden;
.app-header {
@include box(100%, 30px);
}
.app-main {
@include box(100%, calc(100% - 30px));
@include box(100%, 100%);
}
}
</style>

View File

@ -18,10 +18,18 @@ export const checkApi = (): Promise<R<any>> => {
return rq.get<R<any>>('/check', {})
}
export const userListApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/user/list', { params })
}
export const userinfoApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/user/info', { params })
}
export const userinfoAdminApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/user/info/admin', { params })
}
export const userUpdApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/upd', data)
}
@ -30,6 +38,18 @@ export const userUpdPwdApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/upd/pwd', data)
}
export const userUpdAdminApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/upd/admin', data)
}
export const userAddApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/add', data)
}
export const userDisabledApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/disabled', data)
}
export const userDelReq = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/del', data)
}

View File

@ -40,13 +40,21 @@ export const userParamListApi = (): Promise<R<any>> => {
}
/**
*
*
* @returns
*/
export const userParamUpdApi = (data: object): Promise<R<any>> => {
return rq.post<R<any>>('/user/param/upd', data)
}
/**
*
* @returns
*/
export const userParamUpdAdminApi = (data: object): Promise<R<any>> => {
return rq.post<R<any>>('/user/param/upd/admin', data)
}
/**
*
* @param data form
@ -304,6 +312,15 @@ export const articleWordsApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/article/stat/words', { params })
}
/**
*
* @param params
* @returns
*/
export const articleWordsUserApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/article/stat/words/user', { params })
}
/**
*
* @param params
@ -543,6 +560,15 @@ export const pictureStatApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/picture/stat', { params })
}
/**
*
* @param params {id:id}
* @returns
*/
export const pictureStatUserApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/picture/stat/user', { params })
}
/**
*
* @param data

View File

@ -5,7 +5,7 @@ const blossom = {
SYS: {
NAME: 'Blossom',
FULL_NAME: 'BLOSSOM-EDITOR',
VERSION: 'v1.12.0',
VERSION: 'v1.13.0',
//
DOC: 'https://www.wangyunf.com/blossom-doc/index',

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconbl"; /* Project id 4118609 */
src: url('iconfont.woff2?t=1705742748603') format('woff2'),
url('iconfont.woff?t=1705742748603') format('woff'),
url('iconfont.ttf?t=1705742748603') format('truetype');
src: url('iconfont.woff2?t=1706597865404') format('woff2'),
url('iconfont.woff?t=1706597865404') format('woff'),
url('iconfont.ttf?t=1706597865404') format('truetype');
}
.iconbl {
@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.bl-brush-line:before {
content: "\ea4f";
}
.bl-a-morevertical-line:before {
content: "\ea50";
}
.bl-scroll:before {
content: "\e610";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,20 @@
"css_prefix_text": "bl-",
"description": "",
"glyphs": [
{
"icon_id": "24341231",
"name": "brush-line",
"font_class": "brush-line",
"unicode": "ea4f",
"unicode_decimal": 59983
},
{
"icon_id": "24341987",
"name": "more 2-line",
"font_class": "a-morevertical-line",
"unicode": "ea50",
"unicode_decimal": 59984
},
{
"icon_id": "9691336",
"name": "滚轮滚动",

Some files were not shown because too many files have changed in this diff Show More