v1.0.17 #26
@@ -50,6 +50,11 @@ public enum CorePermissionCode implements BuiltinPermissionCode {
|
||||
USER_UPDATE("USER:UPDATE", "用户修改", false, true),
|
||||
USER_DELETE("USER:DELETE", "用户删除", false, true),
|
||||
|
||||
FEEDBACK_CREATE("FEEDBACK:CREATE", "反馈创建", false, true),
|
||||
FEEDBACK_READ("FEEDBACK:READ", "反馈读取", false, true),
|
||||
FEEDBACK_UPDATE("FEEDBACK:UPDATE", "反馈修改", false, true),
|
||||
FEEDBACK_DELETE("FEEDBACK:DELETE", "反馈删除", false, true),
|
||||
|
||||
USER_ROLE_CREATE("USER_ROLE:CREATE", "用户角色创建", true, true),
|
||||
USER_ROLE_READ("USER_ROLE:READ", "用户角色读取", true, true),
|
||||
USER_ROLE_DELETE("USER_ROLE:DELETE", "用户角色删除", true, true);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.imyeyu.api.modules.common.controller;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.imyeyu.api.annotation.RequireCorePermission;
|
||||
import com.imyeyu.api.bean.CorePermissionCode;
|
||||
import com.imyeyu.api.modules.common.entity.Feedback;
|
||||
import com.imyeyu.api.modules.common.service.FeedbackService;
|
||||
import com.imyeyu.api.modules.user.service.UserLoginService;
|
||||
import com.imyeyu.spring.TimiSpring;
|
||||
import com.imyeyu.spring.annotation.AOPLog;
|
||||
import com.imyeyu.spring.annotation.CaptchaValid;
|
||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
||||
import com.imyeyu.spring.bean.CaptchaData;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.util.ResponseView;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// 通用反馈接口
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-19
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/feedback")
|
||||
public class FeedbackController {
|
||||
|
||||
private final FeedbackService service;
|
||||
private final UserLoginService userLoginService;
|
||||
|
||||
/// 提交反馈。登录用户和匿名用户均需提交图形验证码,匿名用户按 IP 限流
|
||||
///
|
||||
/// @param req 带图形验证码的反馈请求
|
||||
/// @return 已创建反馈
|
||||
@AOPLog
|
||||
@JsonView(ResponseView.Public.class)
|
||||
@CaptchaValid
|
||||
@RequestRateLimit(value = 3, inSeconds = 60)
|
||||
@PostMapping("/submit")
|
||||
public Feedback submit(@RequestBody @Valid CaptchaData<Feedback> req) {
|
||||
Feedback feedback = req.getData();
|
||||
feedback.setIp(TimiSpring.getRequestIP());
|
||||
if (userLoginService.getLoginUser() != null) {
|
||||
feedback.setUserId(userLoginService.getLoginUser().getId());
|
||||
}
|
||||
service.create(feedback);
|
||||
return feedback;
|
||||
}
|
||||
|
||||
/// 查询反馈列表
|
||||
///
|
||||
/// @param page 分页参数
|
||||
/// @return 反馈分页
|
||||
@JsonView(ResponseView.Admin.class)
|
||||
@RequestRateLimit
|
||||
@RequireCorePermission(CorePermissionCode.FEEDBACK_READ)
|
||||
@PostMapping("/list")
|
||||
public PageResult<Feedback> list(@RequestBody Page<Feedback> page) {
|
||||
return service.page(page);
|
||||
}
|
||||
|
||||
/// 查询反馈详情
|
||||
///
|
||||
/// @param id 反馈 ID
|
||||
/// @return 反馈详情
|
||||
@JsonView(ResponseView.Admin.class)
|
||||
@RequestRateLimit
|
||||
@RequireCorePermission(CorePermissionCode.FEEDBACK_READ)
|
||||
@PostMapping("/detail")
|
||||
public Feedback detail(@RequestParam String id) {
|
||||
return service.get(id);
|
||||
}
|
||||
|
||||
/// 后台创建反馈
|
||||
///
|
||||
/// @param feedback 反馈
|
||||
/// @return 反馈
|
||||
@AOPLog
|
||||
@JsonView(ResponseView.Admin.class)
|
||||
@RequestRateLimit
|
||||
@RequireCorePermission(CorePermissionCode.FEEDBACK_CREATE)
|
||||
@PostMapping("/create")
|
||||
public Feedback create(@RequestBody Feedback feedback) {
|
||||
service.create(feedback);
|
||||
return feedback;
|
||||
}
|
||||
|
||||
/// 更新反馈
|
||||
///
|
||||
/// @param feedback 反馈
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireCorePermission(CorePermissionCode.FEEDBACK_UPDATE)
|
||||
@PostMapping("/update")
|
||||
public void update(@RequestBody Feedback feedback) {
|
||||
feedback.setRespondedBy(userLoginService.getRequireLoginUserId());
|
||||
service.update(feedback);
|
||||
}
|
||||
|
||||
/// 删除反馈
|
||||
///
|
||||
/// @param id 反馈 ID
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireCorePermission(CorePermissionCode.FEEDBACK_DELETE)
|
||||
@PostMapping("/delete")
|
||||
public void delete(@RequestParam String id) {
|
||||
service.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,10 @@ public class Article extends UUIDEntity implements SettingSupport {
|
||||
public enum BizType {
|
||||
|
||||
/** 博客 */
|
||||
BLOG
|
||||
BLOG,
|
||||
|
||||
/// 反馈
|
||||
FEEDBACK
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,4 +82,7 @@ public class Article extends UUIDEntity implements SettingSupport {
|
||||
|
||||
@Transient
|
||||
protected List<Comment> commentList;
|
||||
|
||||
@Transient
|
||||
protected List<Attachment> attachmentList;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ public class Attachment extends UUIDEntity {
|
||||
/// @since 2026-07-17 20:52
|
||||
public enum BizType {
|
||||
|
||||
ARTICLE,
|
||||
|
||||
USER,
|
||||
|
||||
GAO_CUSTOMER,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.imyeyu.api.modules.common.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.imyeyu.api.bean.ModuleCode;
|
||||
import com.imyeyu.spring.annotation.table.Transient;
|
||||
import com.imyeyu.spring.entity.UUIDEntity;
|
||||
import com.imyeyu.spring.util.ResponseView;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/// 通用反馈
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-19
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Feedback extends UUIDEntity {
|
||||
|
||||
/// 反馈处理状态
|
||||
public enum Status {
|
||||
|
||||
/// 待处理
|
||||
PENDING,
|
||||
|
||||
/// 已回复
|
||||
REPLIED,
|
||||
|
||||
/// 已关闭
|
||||
CLOSED
|
||||
}
|
||||
|
||||
/// 所属模块
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private ModuleCode module;
|
||||
|
||||
/// 提交用户 ID,匿名反馈为 null
|
||||
@JsonView(ResponseView.Admin.class)
|
||||
private String userId;
|
||||
|
||||
/// 反馈内容文章 ID
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private String contentId;
|
||||
|
||||
/// 联系方式
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private String contact;
|
||||
|
||||
/// 提交 IP
|
||||
@JsonView(ResponseView.Admin.class)
|
||||
private String ip;
|
||||
|
||||
/// 处理状态
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private Status status;
|
||||
|
||||
/// 管理员回复文章 ID
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private String responseId;
|
||||
|
||||
/// 回复用户 ID
|
||||
@JsonView(ResponseView.Admin.class)
|
||||
private String respondedBy;
|
||||
|
||||
/// 回复时间
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private Long respondedAt;
|
||||
|
||||
/// 管理员回复文章
|
||||
@Transient
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private Article content;
|
||||
|
||||
/// 管理员回复文章
|
||||
@Transient
|
||||
@JsonView(ResponseView.Public.class)
|
||||
private Article response;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.imyeyu.api.modules.common.mapper;
|
||||
|
||||
import com.imyeyu.api.modules.common.entity.Feedback;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
|
||||
/// 通用反馈 Mapper
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-19
|
||||
public interface FeedbackMapper extends BaseMapper<Feedback, String> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.imyeyu.api.modules.common.service;
|
||||
|
||||
import com.imyeyu.api.modules.common.entity.Feedback;
|
||||
import com.imyeyu.spring.service.BaseService;
|
||||
|
||||
/// 通用反馈服务
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-19
|
||||
public interface FeedbackService extends BaseService<Feedback, String> {
|
||||
}
|
||||
+54
@@ -1,8 +1,15 @@
|
||||
package com.imyeyu.api.modules.common.service.implement;
|
||||
|
||||
import com.imyeyu.api.modules.common.entity.Article;
|
||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||
import com.imyeyu.api.modules.common.mapper.ArticleMapper;
|
||||
import com.imyeyu.api.modules.common.service.ArticleService;
|
||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||
import com.imyeyu.api.modules.system.entity.Logger;
|
||||
import com.imyeyu.api.modules.system.service.LoggerService;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import com.imyeyu.spring.service.AbstractEntityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -20,10 +27,57 @@ import org.springframework.stereotype.Service;
|
||||
@RequiredArgsConstructor
|
||||
public class ArticleServiceImplement extends AbstractEntityService<Article, String> implements ArticleService {
|
||||
|
||||
private final LoggerService loggerService;
|
||||
private final AttachmentService attachmentService;
|
||||
|
||||
private final ArticleMapper mapper;
|
||||
|
||||
@Override
|
||||
protected BaseMapper<Article, String> mapper() {
|
||||
return mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Article article) {
|
||||
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_ARTICLE_CREATE");
|
||||
try {
|
||||
super.create(article);
|
||||
attachmentService.updateByBizId(Attachment.BizType.ARTICLE, article.getId(), article.getAttachmentList());
|
||||
logger.setLevel(Logger.Level.INFO);
|
||||
logger.setResult(article.getId());
|
||||
} catch (TimiException e) {
|
||||
logger.setLevel(Logger.Level.WARN);
|
||||
logger.setException(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.setLevel(Logger.Level.ERROR);
|
||||
logger.setException(TimiJava.serializeThrowable(e));
|
||||
log.error("create article error", e);
|
||||
throw new TimiException(TimiCode.ERROR, "create article error", e);
|
||||
} finally {
|
||||
loggerService.create(logger);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Article article) {
|
||||
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_ARTICLE_UPDATE");
|
||||
try {
|
||||
super.update(article);
|
||||
attachmentService.updateByBizId(Attachment.BizType.ARTICLE, article.getId(), article.getAttachmentList());
|
||||
logger.setLevel(Logger.Level.INFO);
|
||||
logger.setResult(article.getId());
|
||||
} catch (TimiException e) {
|
||||
logger.setLevel(Logger.Level.WARN);
|
||||
logger.setException(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.setLevel(Logger.Level.ERROR);
|
||||
logger.setException(TimiJava.serializeThrowable(e));
|
||||
log.error("update article error", e);
|
||||
throw new TimiException(TimiCode.ERROR, "update article error", e);
|
||||
} finally {
|
||||
loggerService.create(logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.imyeyu.api.modules.common.service.implement;
|
||||
|
||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||
import com.imyeyu.api.modules.common.entity.Feedback;
|
||||
import com.imyeyu.api.modules.common.mapper.FeedbackMapper;
|
||||
import com.imyeyu.api.modules.common.service.ArticleService;
|
||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||
import com.imyeyu.api.modules.common.service.FeedbackService;
|
||||
import com.imyeyu.api.modules.system.entity.Logger;
|
||||
import com.imyeyu.api.modules.system.service.LoggerService;
|
||||
import com.imyeyu.api.modules.user.service.UserLoginService;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import com.imyeyu.spring.service.AbstractEntityService;
|
||||
import com.imyeyu.utils.Time;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/// 通用反馈服务实现
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-19
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class FeedbackServiceImplement extends AbstractEntityService<Feedback, String> implements FeedbackService {
|
||||
|
||||
private static final long ANONYMOUS_ATTACHMENT_MAX_SIZE = 500L * 1024;
|
||||
|
||||
private final LoggerService loggerService;
|
||||
private final ArticleService articleService;
|
||||
private final UserLoginService userLoginService;
|
||||
private final AttachmentService attachmentService;
|
||||
|
||||
private final FeedbackMapper mapper;
|
||||
|
||||
@Override
|
||||
protected BaseMapper<Feedback, String> mapper() {
|
||||
return mapper;
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void create(Feedback feedback) {
|
||||
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_FEEDBACK_CREATE");
|
||||
try {
|
||||
TimiException.required(feedback, "未找到反馈");
|
||||
articleService.create(feedback.getContent());
|
||||
feedback.setContentId(feedback.getContent().getId());
|
||||
feedback.setStatus(TimiJava.defaultIfNull(feedback.getStatus(), Feedback.Status.PENDING));
|
||||
super.create(feedback);
|
||||
logger.setLevel(Logger.Level.INFO);
|
||||
logger.setResult(feedback.getId());
|
||||
} catch (TimiException e) {
|
||||
logger.setLevel(Logger.Level.WARN);
|
||||
logger.setException(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.setLevel(Logger.Level.ERROR);
|
||||
logger.setException(TimiJava.serializeThrowable(e));
|
||||
log.error("create feedback error", e);
|
||||
throw new TimiException(TimiCode.ERROR, "create feedback error", e);
|
||||
} finally {
|
||||
loggerService.create(logger);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void update(Feedback feedback) {
|
||||
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_FEEDBACK_UPDATE");
|
||||
try {
|
||||
TimiException.required(feedback, "未找到反馈");
|
||||
TimiException.required(feedback.getId(), "未找到反馈 ID");
|
||||
Feedback dbFeedback = get(feedback.getId());
|
||||
TimiException.required(dbFeedback, "未找到反馈");
|
||||
|
||||
feedback.getContent().setId(dbFeedback.getContentId());
|
||||
articleService.update(feedback.getContent());
|
||||
|
||||
dbFeedback.setModule(TimiJava.defaultIfNull(feedback.getModule(), dbFeedback.getModule()));
|
||||
if (TimiJava.isNotEmpty(feedback.getResponse())) {
|
||||
if (TimiJava.isEmpty(feedback.getResponseId())) {
|
||||
articleService.create(feedback.getResponse());
|
||||
dbFeedback.setResponseId(feedback.getResponse().getId());
|
||||
dbFeedback.setRespondedAt(Time.now());
|
||||
} else {
|
||||
feedback.getResponse().setId(dbFeedback.getResponseId());
|
||||
articleService.update(feedback.getResponse());
|
||||
}
|
||||
dbFeedback.setStatus(Feedback.Status.REPLIED);
|
||||
dbFeedback.setRespondedBy(userLoginService.getRequireLoginUserId());
|
||||
}
|
||||
super.update(dbFeedback);
|
||||
logger.setLevel(Logger.Level.INFO);
|
||||
logger.setResult(feedback.getId());
|
||||
} catch (TimiException e) {
|
||||
logger.setLevel(Logger.Level.WARN);
|
||||
logger.setException(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.setLevel(Logger.Level.ERROR);
|
||||
logger.setException(TimiJava.serializeThrowable(e));
|
||||
log.error("修改反馈失败", e);
|
||||
throw new TimiException(TimiCode.ERROR, "修改反馈失败", e);
|
||||
} finally {
|
||||
loggerService.create(logger);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_FEEDBACK_DELETE");
|
||||
try {
|
||||
Feedback feedback = get(id);
|
||||
articleService.delete(feedback.getContentId());
|
||||
if (TimiJava.isNotEmpty(feedback.getResponseId())) {
|
||||
articleService.delete(feedback.getResponseId());
|
||||
}
|
||||
super.delete(id);
|
||||
logger.setLevel(Logger.Level.INFO);
|
||||
logger.setResult(id);
|
||||
} catch (TimiException e) {
|
||||
logger.setLevel(Logger.Level.WARN);
|
||||
logger.setException(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.setLevel(Logger.Level.ERROR);
|
||||
logger.setException(TimiJava.serializeThrowable(e));
|
||||
log.error("delete feedback error", e);
|
||||
throw new TimiException(TimiCode.ERROR, "delete feedback error", e);
|
||||
} finally {
|
||||
loggerService.create(logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE `feedback` (
|
||||
`id` CHAR(36) NOT NULL COMMENT '主键',
|
||||
`module` VARCHAR(32) NOT NULL COMMENT '所属模块',
|
||||
`user_id` CHAR(36) NULL COMMENT '提交用户 ID,匿名反馈为空',
|
||||
`content_id` VARCHAR(36) NOT NULL COMMENT '反馈内容文章 ID',
|
||||
`contact` VARCHAR(255) NULL COMMENT '联系方式',
|
||||
`ip` VARCHAR(64) NULL COMMENT '提交 IP',
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '处理状态',
|
||||
`response_id` VARCHAR(36) NULL COMMENT '管理员回复文章 ID',
|
||||
`responded_by` CHAR(36) NULL COMMENT '回复用户 ID',
|
||||
`responded_at` BIGINT NULL COMMENT '回复时间',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_feedback_module_status_created` (`module`, `status`, `created_at`),
|
||||
KEY `idx_feedback_user_created` (`user_id`, `created_at`),
|
||||
KEY `idx_feedback_content_id` (`content_id`),
|
||||
KEY `idx_feedback_response_id` (`response_id`),
|
||||
KEY `idx_feedback_deleted_at` (`deleted_at`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '通用反馈';
|
||||
Reference in New Issue
Block a user