Browse Source

优化通知提醒功能、资质上传功能、文章发布、视频培训功能

master
ChaiNingQi 1 month ago
parent
commit
b068aaf79d
  1. 12
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java
  2. 2
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java
  3. 125
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetKnowledgeController.java
  4. 140
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetNotificationController.java
  5. 316
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetQualificationController.java
  6. 194
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java
  7. 34
      chenhai-common/src/main/java/com/chenhai/common/utils/file/FileUtils.java
  8. 14
      chenhai-system/pom.xml
  9. 286
      chenhai-system/src/main/java/com/chenhai/vet/CertificateRemindTask.java
  10. 117
      chenhai-system/src/main/java/com/chenhai/vet/domain/VetKnowledge.java
  11. 2
      chenhai-system/src/main/java/com/chenhai/vet/domain/VetNotification.java
  12. 188
      chenhai-system/src/main/java/com/chenhai/vet/domain/VetQualification.java
  13. 26
      chenhai-system/src/main/java/com/chenhai/vet/domain/VetTrainingVideo.java
  14. 61
      chenhai-system/src/main/java/com/chenhai/vet/mapper/VetKnowledgeMapper.java
  15. 29
      chenhai-system/src/main/java/com/chenhai/vet/mapper/VetNotificationMapper.java
  16. 3
      chenhai-system/src/main/java/com/chenhai/vet/mapper/VetQualificationMapper.java
  17. 28
      chenhai-system/src/main/java/com/chenhai/vet/mapper/VetTrainingVideoMapper.java
  18. 6
      chenhai-system/src/main/java/com/chenhai/vet/service/IVetCertificateService.java
  19. 74
      chenhai-system/src/main/java/com/chenhai/vet/service/IVetKnowledgeService.java
  20. 1
      chenhai-system/src/main/java/com/chenhai/vet/service/IVetQualificationService.java
  21. 51
      chenhai-system/src/main/java/com/chenhai/vet/service/IVetTrainingVideoService.java
  22. 15
      chenhai-system/src/main/java/com/chenhai/vet/service/VetNotificationService.java
  23. 2
      chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetCertificateServiceImpl.java
  24. 134
      chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetKnowledgeServiceImpl.java
  25. 78
      chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetNotificationServiceImpl.java
  26. 188
      chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetQualificationServiceImpl.java
  27. 146
      chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetTrainingVideoServiceImpl.java
  28. 96
      chenhai-system/src/main/resources/mapper/vet/VetKnowledgeMapper.xml
  29. 51
      chenhai-system/src/main/resources/mapper/vet/VetNotificationMapper.xml
  30. 32
      chenhai-system/src/main/resources/mapper/vet/VetQualificationMapper.xml
  31. 110
      chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml
  32. 4
      chenhai-ui/package.json
  33. 60
      chenhai-ui/src/api/vet/knowledge.js
  34. 95
      chenhai-ui/src/api/vet/training.js
  35. 10
      chenhai-ui/src/views/vet/certificate/index.vue
  36. 356
      chenhai-ui/src/views/vet/knowledge/index.vue
  37. 960
      chenhai-ui/src/views/vet/training/TrainingHome.vue

12
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java

@ -70,6 +70,18 @@ public class VetCertificateController extends BaseController
public TableDataInfo list(VetCertificate vetCertificate) public TableDataInfo list(VetCertificate vetCertificate)
{ {
startPage(); startPage();
Long currentUserId = getCurrentUserId();
if (currentUserId == null) {
return getDataTable(List.of());
}
// 如果是管理员可以查看所有证书不过滤用户ID
if (!isAdmin()) {
// 普通用户只查询自己的证书
vetCertificate.setUserId(currentUserId);
}
// 管理员不设置userId查询所有证书
List<VetCertificate> list = vetCertificateService.selectVetCertificateList(vetCertificate); List<VetCertificate> list = vetCertificateService.selectVetCertificateList(vetCertificate);
return getDataTable(list); return getDataTable(list);
} }

2
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java

@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.*; import java.util.*;
/** /**
* 兽医经验文章Controller
* 兽医经验分享Controller
*/ */
@RestController @RestController
@RequestMapping("/vet/article") @RequestMapping("/vet/article")

125
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetKnowledgeController.java

@ -0,0 +1,125 @@
package com.chenhai.web.controller.vet;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.chenhai.common.annotation.Log;
import com.chenhai.common.core.controller.BaseController;
import com.chenhai.common.core.domain.AjaxResult;
import com.chenhai.common.enums.BusinessType;
import com.chenhai.vet.domain.VetKnowledge;
import com.chenhai.vet.service.IVetKnowledgeService;
import com.chenhai.common.utils.poi.ExcelUtil;
import com.chenhai.common.core.page.TableDataInfo;
/**
* 兽医文章Controller
*
* @author ruoyi
* @date 2026-01-08
*/
@RestController
@RequestMapping("/vet/knowledge")
public class VetKnowledgeController extends BaseController
{
@Autowired
private IVetKnowledgeService vetKnowledgeService;
/**
* 查询兽医文章列表
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:list')")
@GetMapping("/list")
public TableDataInfo list(VetKnowledge vetKnowledge)
{
startPage();
List<VetKnowledge> list = vetKnowledgeService.selectVetKnowledgeList(vetKnowledge);
return getDataTable(list);
}
/**
* 导出兽医文章列表
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:export')")
@Log(title = "兽医文章", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, VetKnowledge vetKnowledge)
{
List<VetKnowledge> list = vetKnowledgeService.selectVetKnowledgeList(vetKnowledge);
ExcelUtil<VetKnowledge> util = new ExcelUtil<VetKnowledge>(VetKnowledge.class);
util.exportExcel(response, list, "兽医文章数据");
}
/**
* 获取兽医文章详细信息
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(vetKnowledgeService.selectVetKnowledgeById(id));
}
/**
* 新增兽医文章
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:add')")
@Log(title = "兽医文章", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody VetKnowledge vetKnowledge)
{
vetKnowledge.setStatus("0");
return toAjax(vetKnowledgeService.insertVetKnowledge(vetKnowledge));
}
/**
* 修改兽医文章
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:edit')")
@Log(title = "兽医文章", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody VetKnowledge vetKnowledge)
{
return toAjax(vetKnowledgeService.updateVetKnowledge(vetKnowledge));
}
/**
* 删除兽医文章
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:remove')")
@Log(title = "兽医文章", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(vetKnowledgeService.deleteVetKnowledgeByIds(ids));
}
/**
* 上传文章待审核
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:upload')")
@Log(title = "兽医文章", businessType = BusinessType.INSERT)
@PostMapping("/upload")
public AjaxResult upload(@RequestBody VetKnowledge vetKnowledge) {
return vetKnowledgeService.uploadVetKnowledge(vetKnowledge);
}
/**
* 发布文章更新状态
*/
@PreAuthorize("@ss.hasPermi('vet:knowledge:publish')")
@Log(title = "兽医文章", businessType = BusinessType.UPDATE)
@PutMapping("/publish/{id}")
public AjaxResult publish(@PathVariable Long id) {
return vetKnowledgeService.publishVetKnowledge(id);
}
}

140
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetNotificationController.java

@ -1,7 +1,9 @@
package com.chenhai.web.controller.vet; package com.chenhai.web.controller.vet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import com.chenhai.common.utils.SecurityUtils; import com.chenhai.common.utils.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -37,7 +39,7 @@ public class VetNotificationController extends BaseController
private VetNotificationService vetNotificationService; private VetNotificationService vetNotificationService;
/** /**
* 获取当前用户ID
* 获取当前用户ID私有方法
*/ */
private Long getCurrentUserId() { private Long getCurrentUserId() {
return SecurityUtils.getUserId(); return SecurityUtils.getUserId();
@ -47,7 +49,8 @@ public class VetNotificationController extends BaseController
* 检查是否为管理员 * 检查是否为管理员
*/ */
private boolean isAdmin() { private boolean isAdmin() {
return SecurityUtils.isAdmin(getCurrentUserId());
Long userId = getCurrentUserId();
return userId != null && SecurityUtils.isAdmin(userId);
} }
/** /**
@ -67,6 +70,64 @@ public class VetNotificationController extends BaseController
return notification != null && currentUserId.equals(notification.getUserId()); return notification != null && currentUserId.equals(notification.getUserId());
} }
/**
* 获取通知统计信息用于首页卡片
*/
@PreAuthorize("@ss.hasPermi('vet:notification:query')")
@GetMapping("/stats/card")
public AjaxResult getNotificationCardStats() {
Long currentUserId = getCurrentUserId();
if (currentUserId == null) {
return success(Map.of(
"unreadCount", 0,
"readCount", 0,
"totalCount", 0
));
}
Map<String, Integer> stats;
if (isAdmin()) {
// 管理员查询所有通知
stats = getAllNotificationStats();
} else {
// 普通用户调用 Service 方法
stats = vetNotificationService.getNotificationStats(currentUserId);
}
// 为前端卡片准备数据
Map<String, Object> result = new HashMap<>();
result.put("unreadCount", stats.get("unread"));
result.put("readCount", stats.get("read"));
result.put("totalCount", stats.get("unread") + stats.get("read"));
return success(result);
}
/**
* 管理员获取所有通知统计私有方法
*/
private Map<String, Integer> getAllNotificationStats() {
try {
// 查询所有通知
VetNotification query = new VetNotification();
List<VetNotification> allNotifications = vetNotificationService.selectVetNotificationList(query);
int total = allNotifications != null ? allNotifications.size() : 0;
// 查询所有未读通知
VetNotification unreadQuery = new VetNotification();
unreadQuery.setIsRead(0);
List<VetNotification> unreadList = vetNotificationService.selectVetNotificationList(unreadQuery);
int unread = unreadList != null ? unreadList.size() : 0;
int read = total - unread;
return Map.of("unread", unread, "read", read);
} catch (Exception e) {
// 出错时返回0
return Map.of("unread", 0, "read", 0);
}
}
/** /**
* 查询兽医通知列表 * 查询兽医通知列表
*/ */
@ -139,9 +200,16 @@ public class VetNotificationController extends BaseController
@PutMapping @PutMapping
public AjaxResult edit(@RequestBody VetNotification vetNotification) public AjaxResult edit(@RequestBody VetNotification vetNotification)
{ {
Long currentUserId = getCurrentUserId();
if (currentUserId == null) {
return error("用户未登录");
}
vetNotification.setUserId(currentUserId);
vetNotification.setCreateBy(SecurityUtils.getUsername());
return toAjax(vetNotificationService.updateVetNotification(vetNotification)); return toAjax(vetNotificationService.updateVetNotification(vetNotification));
} }
/** /**
* 删除兽医通知 * 删除兽医通知
*/ */
@ -153,63 +221,15 @@ public class VetNotificationController extends BaseController
return toAjax(vetNotificationService.deleteVetNotificationByIds(ids)); return toAjax(vetNotificationService.deleteVetNotificationByIds(ids));
} }
/**
* 获取当前用户的未读通知数量
*/
@PreAuthorize("@ss.hasPermi('vet:notification:query')")
@GetMapping("/user/unread/count")
public AjaxResult getUnreadCount()
{
Long currentUserId = getUserId();
if (currentUserId == null) {
return success(0);
}
int unreadCount = vetNotificationService.getUnreadCount(currentUserId);
return success(unreadCount);
}
/**
* 标记单个通知为已读
*/
@PreAuthorize("@ss.hasPermi('vet:notification:edit')")
@Log(title = "标记通知已读", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/read")
public AjaxResult markAsRead(@PathVariable Long id)
{
boolean result = vetNotificationService.markAsRead(id);
return toAjax(result ? 1 : 0);
}
/**
* 标记所有通知为已读
*/
@PreAuthorize("@ss.hasPermi('vet:notification:edit')")
@Log(title = "标记所有通知已读", businessType = BusinessType.UPDATE)
@PutMapping("/user/mark-all-read")
public AjaxResult markAllAsRead()
{
Long currentUserId = getUserId();
if (currentUserId == null) {
return error("用户未登录");
/**
* 标记单个通知为已读
*/
@PreAuthorize("@ss.hasPermi('vet:notification:edit')")
@Log(title = "标记通知已读", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/read")
public AjaxResult markAsRead (@PathVariable Long id)
{
boolean result = vetNotificationService.markAsRead(id);
return toAjax(result ? 1 : 0);
} }
boolean result = vetNotificationService.markAllAsRead(currentUserId);
return toAjax(result ? 1 : 0);
}
/**
* 根据指定用户ID查询通知列表管理员功能
*/
@PreAuthorize("@ss.hasPermi('vet:notification:list')")
@GetMapping("/list/user/{userId}")
public TableDataInfo listByUserId(@PathVariable Long userId)
{
startPage();
VetNotification query = new VetNotification();
query.setUserId(userId);
List<VetNotification> list = vetNotificationService.selectVetNotificationList(query);
return getDataTable(list);
}
} }

316
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetQualificationController.java

@ -6,24 +6,32 @@ import com.chenhai.common.core.domain.AjaxResult;
import com.chenhai.common.core.page.TableDataInfo; import com.chenhai.common.core.page.TableDataInfo;
import com.chenhai.common.enums.BusinessType; import com.chenhai.common.enums.BusinessType;
import com.chenhai.common.utils.SecurityUtils; import com.chenhai.common.utils.SecurityUtils;
import com.chenhai.common.utils.file.FileUtils;
import com.chenhai.common.utils.poi.ExcelUtil; import com.chenhai.common.utils.poi.ExcelUtil;
import com.chenhai.vet.domain.BusinessScopeConstants; import com.chenhai.vet.domain.BusinessScopeConstants;
import com.chenhai.vet.domain.VetCertificate;
import com.chenhai.vet.domain.VetQualification; import com.chenhai.vet.domain.VetQualification;
import com.chenhai.vet.mapper.VetQualificationMapper;
import com.chenhai.vet.service.IVetQualificationService; import com.chenhai.vet.service.IVetQualificationService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import static com.chenhai.framework.datasource.DynamicDataSourceContextHolder.log;
/** /**
* 兽医资质Controller
*
* @author ruoyi
* @date 2025-12-26
* 兽医资质控制器
*
* @author 自定义
*/ */
@RestController @RestController
@RequestMapping("/vet/qualification") @RequestMapping("/vet/qualification")
@ -31,8 +39,124 @@ public class VetQualificationController extends BaseController
{ {
@Autowired @Autowired
private IVetQualificationService vetQualificationService; private IVetQualificationService vetQualificationService;
@Autowired @Autowired
private VetQualificationMapper vetQualificationMapper;
private JdbcTemplate jdbcTemplate;
// 注入若依配置的文件上传根路径需在application.yml配置ruoyi.profile=D:/ruoyi/uploadPath
@Value("${chenhai.profile}")
private String uploadRootPath;
// 资质文件存储子路径便于分类管理
private static final String QUALIFICATION_FILE_PATH = "/vet/qualification/";
/**
* 获取资质类型选项下拉框用
*/
@GetMapping("/type/options")
public AjaxResult getQualificationTypeOptions() {
String sql = "SELECT dict_value as value, dict_label as label " +
"FROM sys_dict_data " +
"WHERE dict_type = 'qualification_type' AND status = '0' " +
"ORDER BY dict_sort";
List<Map<String, String>> options = jdbcTemplate.query(sql, (rs, rowNum) -> {
Map<String, String> option = new HashMap<>();
option.put("value", rs.getString("value"));
option.put("label", rs.getString("label"));
return option;
});
return AjaxResult.success(options);
}
/**
* 获取经营范围选项下拉框用
*/
@GetMapping("/scope/options")
public AjaxResult getScopeOptions() {
String sql = "SELECT dict_value as value, dict_label as label " +
"FROM sys_dict_data " +
"WHERE dict_type = 'scope_names' AND status = '0' " +
"ORDER BY dict_sort";
List<Map<String, String>> options = jdbcTemplate.query(sql, (rs, rowNum) -> {
Map<String, String> option = new HashMap<>();
option.put("value", rs.getString("value"));
option.put("label", rs.getString("label"));
return option;
});
return AjaxResult.success(options);
}
/**
* 上传资质文件关联兽医资质ID
* @param file 上传的文件
* @return 文件上传结果包含访问路径存储路径等
*/
@PreAuthorize("@ss.hasPermi('vet:qualification:edit')")
@Log(title = "兽医资质文件上传", businessType = BusinessType.UPDATE)
@PostMapping("/upload")
public AjaxResult uploadQualificationFile(@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty()) {
return AjaxResult.error("文件不能为空");
}
String originalFileName = file.getOriginalFilename();
String ext = FileUtils.getExtension(originalFileName).toLowerCase();
// 文件类型验证
String[] allowedExts = {"jpg", "png", "pdf", "doc", "docx", "xls", "xlsx"};
boolean isAllowed = FileUtils.isAllowedExtension(originalFileName, allowedExts);
if (!isAllowed) {
return AjaxResult.error("文件类型不允许");
}
// 🔥 关键使用配置的路径
String basePath = uploadRootPath; // D:/ymtx/uploadPath
String subPath = "vet/qualification";
String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
String fullPath = basePath + File.separator + subPath + File.separator + datePath;
// 创建目录
File destDir = new File(fullPath);
if (!destDir.exists()) {
destDir.mkdirs();
}
// 生成唯一文件名
String uuid = UUID.randomUUID().toString().replace("-", "");
String fileName = uuid + "_" + originalFileName;
String filePath = fullPath + File.separator + fileName;
// 保存文件
File destFile = new File(filePath);
file.transferTo(destFile);
// 🔥 返回可访问的URL注意路径格式
// 前端访问路径/profile/vet/qualification/yyyy/MM/dd/xxx.jpg
String fileUrl = "/" + subPath + "/" + datePath + "/" + fileName;
// 下载链接Spring有默认的下载控制器 /common/download?fileName=xxx
String downloadUrl = "/common/download?fileName=" + URLEncoder.encode(fileUrl, "UTF-8");
Map<String, Object> data = new HashMap<>();
data.put("fileName", originalFileName);
data.put("fileUrl", fileUrl); // 前端显示用
data.put("downloadUrl", downloadUrl); // 下载用
data.put("filePath", fileUrl); // 提交到资质表用存储相对路径
data.put("fileSize", file.getSize());
data.put("uuid", uuid);
return AjaxResult.success("上传成功", data);
} catch (Exception e) {
log.error("文件上传失败", e);
return AjaxResult.error("上传失败: " + e.getMessage());
}
}
@ -42,36 +166,32 @@ public class VetQualificationController extends BaseController
@PreAuthorize("@ss.hasPermi('vet:qualification:list')") @PreAuthorize("@ss.hasPermi('vet:qualification:list')")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(VetQualification vetQualification) public TableDataInfo list(VetQualification vetQualification)
{ startPage();
{
startPage();
Long userId = SecurityUtils.getUserId(); Long userId = SecurityUtils.getUserId();
if (!SecurityUtils.isAdmin(userId)) { if (!SecurityUtils.isAdmin(userId)) {
vetQualification.setVetId(userId);
vetQualification.setUserId(userId);
} }
List<VetQualification> list = vetQualificationService.selectVetQualificationList(vetQualification); List<VetQualification> list = vetQualificationService.selectVetQualificationList(vetQualification);
return getDataTable(list); return getDataTable(list);
} }
/** /**
* 登录后检查是否需要填写资质弹窗用
* 登录后检查是否需要填写资质
*/ */
@GetMapping("/checkNeedQualification") @GetMapping("/checkNeedQualification")
public AjaxResult checkNeedQualification() { public AjaxResult checkNeedQualification() {
Long userId = SecurityUtils.getUserId(); Long userId = SecurityUtils.getUserId();
VetQualification query = new VetQualification(); VetQualification query = new VetQualification();
query.setVetId(userId);
query.setUserId(userId);
List<VetQualification> list = vetQualificationService.selectVetQualificationList(query); List<VetQualification> list = vetQualificationService.selectVetQualificationList(query);
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
if (list.isEmpty()) { if (list.isEmpty()) {
// 没有资质记录需要弹窗填写
result.put("needPopup", true); result.put("needPopup", true);
result.put("message", "请先填写兽医资质信息"); result.put("message", "请先填写兽医资质信息");
} else { } else {
VetQualification qualification = list.get(0); VetQualification qualification = list.get(0);
// 检查资质状态
if ("0".equals(qualification.getAuditStatus())) { if ("0".equals(qualification.getAuditStatus())) {
result.put("needPopup", false); result.put("needPopup", false);
result.put("message", "您的资质正在审核中,请耐心等待"); result.put("message", "您的资质正在审核中,请耐心等待");
@ -90,68 +210,9 @@ public class VetQualificationController extends BaseController
result.put("status", "needSubmit"); result.put("status", "needSubmit");
} }
} }
return AjaxResult.success(result); return AjaxResult.success(result);
} }
/**
* 快速提交资质弹窗用
*/
@Log(title = "快速提交资质", businessType = BusinessType.INSERT)
@PostMapping("/quickSubmit")
public AjaxResult quickSubmit(@RequestBody VetQualification vetQualification) {
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
// 设置当前用户信息
vetQualification.setVetId(userId);
vetQualification.setCreateBy(username);
// 处理经营范围名称
if (vetQualification.getScopeIds() != null && !vetQualification.getScopeIds().isEmpty()) {
String[] scopeIds = vetQualification.getScopeIds().split(",");
List<String> scopeNames = new ArrayList<>();
for (String scopeId : scopeIds) {
String name = BusinessScopeConstants.getScopeName(scopeId.trim());
if (name != null) {
scopeNames.add(name);
}
}
vetQualification.setScopeNames(String.join(",", scopeNames));
}
// 设置为待审核状态
vetQualification.setAuditStatus("0");
int result;
if (vetQualification.getQualificationId() != null) {
// 更新
vetQualification.setUpdateBy(username);
result = vetQualificationService.updateVetQualification(vetQualification);
} else {
// 新增
result = vetQualificationService.insertVetQualification(vetQualification);
}
return toAjax(result);
}
/**
* 获取简单的经营范围列表下拉框用
*/
@GetMapping("/scope/options")
public AjaxResult getScopeOptions() {
List<Map<String, String>> options = new ArrayList<>();
for (Map.Entry<String, String> entry : BusinessScopeConstants.SCOPE_MAP.entrySet()) {
Map<String, String> option = new HashMap<>();
option.put("value", entry.getKey());
option.put("label", entry.getValue());
options.add(option);
}
return AjaxResult.success(options);
}
/** /**
* 获取经营范围详情 * 获取经营范围详情
@ -162,40 +223,8 @@ public class VetQualificationController extends BaseController
detail.put("scopeId", scopeId); detail.put("scopeId", scopeId);
detail.put("scopeName", BusinessScopeConstants.getScopeName(scopeId)); detail.put("scopeName", BusinessScopeConstants.getScopeName(scopeId));
detail.put("requiredQualifications", BusinessScopeConstants.getRequiredQualifications(scopeId)); detail.put("requiredQualifications", BusinessScopeConstants.getRequiredQualifications(scopeId));
return AjaxResult.success(detail); return AjaxResult.success(detail);
} }
/**
* 获取我的资质信息
*/
@GetMapping("/my")
public AjaxResult getMyQualification() {
Long userId = SecurityUtils.getUserId();
VetQualification query = new VetQualification();
query.setVetId(userId);
List<VetQualification> list = vetQualificationService.selectVetQualificationList(query);
if (!list.isEmpty()) {
VetQualification qualification = list.get(0);
// 处理经营范围名称
if (qualification.getScopeIds() != null && !qualification.getScopeIds().isEmpty()
&& (qualification.getScopeNames() == null || qualification.getScopeNames().isEmpty())) {
String[] scopeIds = qualification.getScopeIds().split(",");
List<String> scopeNames = new ArrayList<>();
for (String scopeId : scopeIds) {
String name = BusinessScopeConstants.getScopeName(scopeId.trim());
if (name != null) {
scopeNames.add(name);
}
}
qualification.setScopeNames(String.join(",", scopeNames));
}
return success(qualification);
}
return AjaxResult.error("暂无资质信息");
}
/** /**
* 导出兽医资质列表 * 导出兽医资质列表
@ -228,14 +257,28 @@ public class VetQualificationController extends BaseController
@PostMapping @PostMapping
public AjaxResult add(@RequestBody VetQualification vetQualification) public AjaxResult add(@RequestBody VetQualification vetQualification)
{ {
// 自动获取当前用户信息
Long userId = SecurityUtils.getUserId();
Long sysUserId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername(); String username = SecurityUtils.getUsername();
try {
vetQualification.setUserId(sysUserId);
vetQualification.setCreateBy(username);
return toAjax(vetQualificationService.insertVetQualification(vetQualification));
} catch (DataIntegrityViolationException e) {
if (e.getMessage().contains("foreign key constraint")) {
createSimpleVetUser(sysUserId, username);
return toAjax(vetQualificationService.insertVetQualification(vetQualification));
}
throw e;
}
}
vetQualification.setVetId(userId);
vetQualification.setCreateBy(username);
return toAjax(vetQualificationService.insertVetQualification(vetQualification));
private void createSimpleVetUser(Long sysUserId, String username) {
try {
String sql = "INSERT IGNORE INTO vet_user (sys_user_id, username, create_time) VALUES (?, ?, NOW())";
jdbcTemplate.update(sql, sysUserId, username);
} catch (Exception e) {
// 忽略异常
}
} }
/** /**
@ -254,7 +297,7 @@ public class VetQualificationController extends BaseController
*/ */
@PreAuthorize("@ss.hasPermi('vet:qualification:remove')") @PreAuthorize("@ss.hasPermi('vet:qualification:remove')")
@Log(title = "兽医资质", businessType = BusinessType.DELETE) @Log(title = "兽医资质", businessType = BusinessType.DELETE)
@DeleteMapping("/{qualificationIds}")
@DeleteMapping("/{qualificationIds}")
public AjaxResult remove(@PathVariable Long[] qualificationIds) public AjaxResult remove(@PathVariable Long[] qualificationIds)
{ {
return toAjax(vetQualificationService.deleteVetQualificationByQualificationIds(qualificationIds)); return toAjax(vetQualificationService.deleteVetQualificationByQualificationIds(qualificationIds));
@ -263,29 +306,48 @@ public class VetQualificationController extends BaseController
/** /**
* 提交审核 * 提交审核
*/ */
/*@PreAuthorize("@ss.hasPermi('vet:qualification:audit')")*/
@Log(title = "兽医资质", businessType = BusinessType.UPDATE) @Log(title = "兽医资质", businessType = BusinessType.UPDATE)
@PostMapping("/submitAudit/{qualificationId}")
public AjaxResult submitAudit(@PathVariable("qualificationId") Long qualificationId) {
VetQualification vetQualification = new VetQualification();
vetQualification.setQualificationId(qualificationId);
vetQualification.setAuditStatus("0"); // 设置为待审核状态
vetQualification.setApplyTime(new Date()); // 设置申请时间为当前时间
@PostMapping("/submit")
public AjaxResult submitQualification(@RequestBody VetQualification vetQualification) {
try {
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
// 设置必要字段
vetQualification.setUserId(userId);
vetQualification.setCreateBy(username);
vetQualification.setAuditStatus("0"); // 设置为待审核状态
vetQualification.setApplyTime(new Date()); // 设置申请时间
int result;
// 如果已有资质ID则为更新操作重新提交审核
if (vetQualification.getQualificationId() != null) {
vetQualification.setUpdateBy(username);
result = vetQualificationService.updateVetQualification(vetQualification);
} else {
// 新增资质记录并提交审核
result = vetQualificationService.insertVetQualification(vetQualification);
}
return toAjax(vetQualificationService.updateVetQualification(vetQualification));
if (result > 0) {
return AjaxResult.success("资质提交成功,等待审核", vetQualification);
} else {
return AjaxResult.error("资质提交失败");
}
} catch (Exception e) {
log.error("提交资质失败", e);
return AjaxResult.error("提交失败:" + e.getMessage());
}
} }
/** /**
* 审核操作 * 审核操作
*/ */
/*@PreAuthorize("@ss.hasPermi('vet:qualification:audit')")*/
@Log(title = "兽医资质", businessType = BusinessType.UPDATE) @Log(title = "兽医资质", businessType = BusinessType.UPDATE)
@PostMapping("/audit") @PostMapping("/audit")
public AjaxResult audit(@RequestBody VetQualification vetQualification) { public AjaxResult audit(@RequestBody VetQualification vetQualification) {
// 设置审核时间为当前时间
vetQualification.setAuditTime(new Date()); vetQualification.setAuditTime(new Date());
// 这里可以从SecurityContextHolder获取当前登录用户ID作为审核人ID
// vetQualification.setAuditorId(getUserId());
return toAjax(vetQualificationService.updateVetQualification(vetQualification)); return toAjax(vetQualificationService.updateVetQualification(vetQualification));
} }
}
}

194
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java

@ -0,0 +1,194 @@
package com.chenhai.web.controller.vet;
import com.chenhai.common.core.controller.BaseController;
import com.chenhai.common.core.domain.AjaxResult;
import com.chenhai.common.core.page.TableDataInfo;
import com.chenhai.common.utils.SecurityUtils;
import com.chenhai.vet.domain.VetTrainingVideo;
import com.chenhai.vet.service.IVetTrainingVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@RequestMapping("/vet/training")
public class VetTrainingVideoController extends BaseController {
@Autowired
private IVetTrainingVideoService trainingVideoService;
/**
* 获取当前用户ID
*/
private Long getCurrentUserId() {
return SecurityUtils.getUserId();
}
/**
* 上传培训视频
*/
@PostMapping("/upload")
public AjaxResult uploadVideo(
@RequestParam String title,
@RequestParam MultipartFile videoFile,
@RequestParam(required = false) String description,
@RequestParam(required = false) String category,
@RequestParam(required = false) String tags,
@RequestParam(required = false, defaultValue = "1") String status,
@RequestParam(required = false) MultipartFile coverImage) {
Long userId = getCurrentUserId();
try {
VetTrainingVideo video = new VetTrainingVideo();
video.setUserId(userId); // 修正变量名
video.setTitle(title);
video.setDescription(description);
video.setCategory(category);
video.setTags(tags);
video.setStatus(status);
// 上传并保存视频
String result = trainingVideoService.uploadAndSave(video, videoFile, coverImage);
return success(result);
} catch (Exception e) {
return error("上传失败:" + e.getMessage());
}
}
/**
* 批量上传视频可选功能
*/
@PostMapping("/upload/batch")
public AjaxResult uploadBatch(
@RequestParam String[] titles,
@RequestParam MultipartFile[] videoFiles,
@RequestParam(required = false) String[] categories) {
Long userId = getCurrentUserId();
try {
int successCount = 0;
for (int i = 0; i < videoFiles.length; i++) {
if (i < titles.length) {
VetTrainingVideo video = new VetTrainingVideo();
video.setUserId(userId); // 修正变量名
video.setTitle(titles[i]);
video.setCategory(categories != null && i < categories.length ? categories[i] : null);
video.setStatus("1");
trainingVideoService.uploadAndSave(video, videoFiles[i], null);
successCount++;
}
}
return success("成功上传 " + successCount + " 个视频");
} catch (Exception e) {
return error("批量上传失败:" + e.getMessage());
}
}
/**
* 查看我上传的视频
*/
@GetMapping("/my-videos")
public TableDataInfo getMyVideos(
@RequestParam(required = false) String title,
@RequestParam(required = false) String category,
@RequestParam(required = false) String status) {
startPage();
Long userId = getCurrentUserId(); // 修正变量名
List<VetTrainingVideo> list = trainingVideoService.getMyVideos(userId, title, category, status);
return getDataTable(list);
}
/**
* 查看所有公开的视频
*/
@GetMapping("/public-videos")
public TableDataInfo getPublicVideos(
@RequestParam(required = false) String title,
@RequestParam(required = false) String category,
@RequestParam(required = false) String userName) { // 参数名改为userName
startPage();
List<VetTrainingVideo> list = trainingVideoService.getPublicVideos(title, category, userName);
return getDataTable(list);
}
/**
* 查看视频详情
*/
@GetMapping("/video/{videoId}")
public AjaxResult getVideoDetail(@PathVariable Long videoId) {
Long userId = getCurrentUserId(); // 修正变量名
VetTrainingVideo video = trainingVideoService.getVideoDetail(videoId, userId);
if (video == null) {
return error("视频不存在或无权限查看");
}
// 如果是公开视频或自己的视频增加观看次数
if ("1".equals(video.getStatus()) || userId.equals(video.getUserId())) {
trainingVideoService.incrementViewCount(videoId);
video.setViewCount(video.getViewCount() + 1);
}
return success(video);
}
/**
* 获取视频播放地址带权限校验- 可选功能
*/
@GetMapping("/video/play/{videoId}")
public AjaxResult getPlayUrl(@PathVariable Long videoId) {
Long userId = getCurrentUserId(); // 修正变量名
String playUrl = trainingVideoService.getVideoPlayUrl(videoId, userId);
if (playUrl == null) {
return error("无权限播放此视频");
}
return success(playUrl);
}
/**
* 获取热门视频按观看次数排序
*/
@GetMapping("/hot-videos")
public AjaxResult getHotVideos(@RequestParam(defaultValue = "10") Integer limit) {
List<VetTrainingVideo> list = trainingVideoService.getHotVideos(limit);
return success(list);
}
/**
* 搜索视频
*/
@GetMapping("/search")
public TableDataInfo searchVideos(@RequestParam String keyword) {
startPage();
List<VetTrainingVideo> list = trainingVideoService.searchVideos(keyword);
return getDataTable(list);
}
/**
* 删除我的视频
*/
@DeleteMapping("/{videoId}")
public AjaxResult deleteVideo(@PathVariable Long videoId) {
Long userId = getCurrentUserId(); // 修正变量名
VetTrainingVideo video = trainingVideoService.getVideoDetail(videoId, userId);
if (video == null) {
return error("视频不存在");
}
if (!userId.equals(video.getUserId())) {
return error("无权删除此视频");
}
return toAjax(trainingVideoService.deleteVideoById(videoId));
}
}

34
chenhai-common/src/main/java/com/chenhai/common/utils/file/FileUtils.java

@ -300,4 +300,38 @@ public class FileUtils
String baseName = FilenameUtils.getBaseName(fileName); String baseName = FilenameUtils.getBaseName(fileName);
return baseName; return baseName;
} }
public static String getExtension(String fileName) {
if (fileName == null) return "";
// 第一步提取纯文件名去掉路径比如 D:/upload/简历.doc 简历.doc
String pureFileName = getName(fileName);
if (pureFileName == null || !pureFileName.contains(".")) return "";
// 第二步去空格 + 取最后一个.后的扩展名
String ext = pureFileName.substring(pureFileName.lastIndexOf(".") + 1).trim();
return ext;
}
// 2. 修复isAllowedExtension补充空值/空字符串校验
public static boolean isAllowedExtension(String fileName, String[] allowedExts) {
if (fileName == null || allowedExts == null || allowedExts.length == 0) return false;
// 获取扩展名并转小写统一匹配规则
String ext = getExtension(fileName).toLowerCase();
// 排除空扩展名情况
if (ext.isEmpty()) return false;
// 遍历允许的扩展名也转小写匹配
for (String allowedExt : allowedExts) {
if (ext.equals(allowedExt.toLowerCase().trim())) {
return true;
}
}
return false;
}
public static boolean mkdirs(String dirPath) {
if (dirPath == null) return false;
File dir = new File(dirPath);
// 目录不存在则创建存在则返回true
return dir.exists() || dir.mkdirs();
}
} }

14
chenhai-system/pom.xml

@ -61,6 +61,20 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<!-- Spring Web包含文件上传支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 如果需要处理文件路径 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

286
chenhai-system/src/main/java/com/chenhai/vet/CertificateRemindTask.java

@ -1,55 +1,285 @@
package com.chenhai.vet; package com.chenhai.vet;
import com.chenhai.vet.domain.VetCertificate; import com.chenhai.vet.domain.VetCertificate;
import com.chenhai.vet.service.IVetCertificateService;
import com.chenhai.vet.domain.VetNotification;
import com.chenhai.vet.mapper.VetCertificateMapper;
import com.chenhai.vet.mapper.VetNotificationMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/**
* 证书提醒定时任务
*
* @author ruoyi
*/
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@Component @Component
@EnableScheduling @EnableScheduling
@Slf4j @Slf4j
public class CertificateRemindTask { public class CertificateRemindTask {
@Autowired @Autowired
private IVetCertificateService vetCertificateService;
private VetCertificateMapper vetCertificateMapper;
@Autowired
private VetNotificationMapper vetNotificationMapper;
// 测试每分钟执行 | 正式每天上午9点 @Scheduled(cron = "0 0 9 * * ?")
//@Scheduled(cron = "0 */1 * * * ?")
@Scheduled(cron = "0 0 9 * * ?")
public void dailyCertificateCheck() {
/* log.info("开始检查证书过期情况...");*/
try {
List<VetCertificate> certificates = vetCertificateMapper.selectVetCertificateList(new VetCertificate());
certificates.forEach(this::checkAndSendReminder);
/* log.info("证书检查完成,共检查{}个证书", certificates.size());*/
} catch (Exception e) {
/*log.error("证书检查任务执行失败", e);*/
}
}
/** /**
* 每天凌晨2点检查证书过期情况
* 检查单个证书并发送提醒
*/ */
/* @Scheduled(cron = "0 0 2 * * ?")*/
@Scheduled(cron = "0 */1 * * * ?")
public void dailyCertificateCheck() {
/*log.info("开始执行每日证书检查任务...");*/
private void checkAndSendReminder(VetCertificate certificate) {
if (certificate == null || certificate.getExpireDate() == null) {
return;
}
long daysRemaining = calculateDayDifference(new Date(), certificate.getExpireDate());
updateCertificateStatus(certificate, daysRemaining);
if (!shouldSendReminder(certificate, daysRemaining) || hasRecentReminder(certificate, daysRemaining)) {
return;
}
sendReminder(certificate, daysRemaining);
}
/**
* 发送提醒
*/
private void sendReminder(VetCertificate certificate, long daysRemaining) {
VetNotification notification = createBaseNotification(certificate);
if (daysRemaining <= 0) {
// 只发送过期当天的提醒
if (daysRemaining == 0) {
setExpiredContent(notification, certificate, 0);
notification.setRemindLevel(3); // 最高级别
vetNotificationMapper.insertVetNotification(notification);
}
// 过期后不再提醒
return;
} else if (daysRemaining <= 7) {
setCountdownContent(notification, certificate, daysRemaining);
notification.setRemindLevel(daysRemaining <= 3 ? 3 : 2);
} else if (daysRemaining == 15 || daysRemaining == 30) {
setPreExpireContent(notification, certificate, daysRemaining);
notification.setRemindLevel(daysRemaining == 30 ? 1 : 2);
} else {
return;
}
vetNotificationMapper.insertVetNotification(notification);
}
/**
* 创建基础通知对象
*/
private VetNotification createBaseNotification(VetCertificate certificate) {
VetNotification notification = new VetNotification();
notification.setUserId(certificate.getUserId());
notification.setRelatedId(certificate.getId().toString());
notification.setType("CERT_EXPIRE_REMIND");
notification.setIsRead(0);
notification.setCreateTime(new Date());
return notification;
}
/**
* 设置过期内容
*/
private void setExpiredContent(VetNotification notification, VetCertificate certificate, long daysExpired) {
if (daysExpired == 0) {
notification.setTitle("🚨 证书今天过期!");
notification.setContent(String.format("您的《%s》证书今天已过期!请立即更新。", certificate.getCertName()));
}
// 移除过期多天的提醒
}
/**
* 设置倒计时内容
*/
private void setCountdownContent(VetNotification notification, VetCertificate certificate, long daysRemaining) {
notification.setTitle("⚠️ 证书还剩" + daysRemaining + "天过期");
notification.setContent(String.format("您的《%s》证书还剩%d天过期,请及时更新。", certificate.getCertName(), daysRemaining));
}
/**
* 设置预过期内容
*/
private void setPreExpireContent(VetNotification notification, VetCertificate certificate, long daysRemaining) {
String timeText = daysRemaining == 30 ? "30天" : "15天";
notification.setTitle("📅 证书还剩" + timeText + "过期");
notification.setContent(String.format("您的《%s》证书将在%s后过期,请提前准备更新。", certificate.getCertName(), timeText));
}
/**
* 判断是否需要发送提醒
*/
private boolean shouldSendReminder(VetCertificate certificate, long daysRemaining) {
String status = certificate.getStatus();
if ("2".equals(status)) { // 已过期
// 只在过期的第一天提醒daysRemaining = 0 刚刚过期时
return daysRemaining == 0; // 只提醒过期当天
}
if ("1".equals(status)) { // 即将过期
return daysRemaining == 30 || daysRemaining == 15 || (daysRemaining >= 1 && daysRemaining <= 7);
}
if ("0".equals(status)) { // 正常
return daysRemaining == 30 || daysRemaining == 15;
}
return false;
}
/**
* 检查最近是否已发送过提醒
*/
private boolean hasRecentReminder(VetCertificate certificate, long daysRemaining) {
try { try {
vetCertificateService.checkAndSendCertificateReminders();
/* log.info("每日证书检查任务执行完成");*/
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Date startOfDay = cal.getTime();
cal.add(Calendar.DAY_OF_MONTH, 1);
Date endOfDay = cal.getTime();
// 检查今天是否已发送
VetNotification query = new VetNotification();
query.setRelatedId(certificate.getId().toString());
query.setType("CERT_EXPIRE_REMIND");
List<VetNotification> notifications = vetNotificationMapper.selectVetNotificationList(query);
// 获取今天的所有通知
List<VetNotification> todayNotifications = notifications.stream()
.filter(n -> n.getCreateTime() != null &&
n.getCreateTime().after(startOfDay) &&
n.getCreateTime().before(endOfDay))
.toList();
// 如果没有今天的通知直接返回
if (todayNotifications.isEmpty()) {
return false;
}
// 生成当前应该发送的通知标题
String expectedTitle = generateExpectedTitle(certificate, daysRemaining);
// 检查今天是否已有相同标题的通知
boolean hasSameTitleToday = todayNotifications.stream()
.anyMatch(n -> expectedTitle.equals(n.getTitle()));
if (hasSameTitleToday) {
// 如果今天已有相同标题的通知跳过
/*log.debug("今天已发送过相同提醒,跳过: 证书ID={}, 标题={}",
certificate.getId(), expectedTitle);*/
return true;
}
// 已过期证书检查最近7天是否有相同标题的通知
if (daysRemaining <= 0) {
cal.add(Calendar.DAY_OF_MONTH, -7);
Date sevenDaysAgo = cal.getTime();
List<VetNotification> recentNotifications = notifications.stream()
.filter(n -> n.getCreateTime() != null &&
n.getCreateTime().after(sevenDaysAgo))
.toList();
boolean hasSameTitleRecent = recentNotifications.stream()
.anyMatch(n -> expectedTitle.equals(n.getTitle()));
if (hasSameTitleRecent) {
/*log.debug("最近7天内已发送过相同提醒,跳过: 证书ID={}", certificate.getId());*/
return true;
}
}
return false;
} catch (Exception e) { } catch (Exception e) {
log.error("每日证书检查任务执行失败", e);
/*log.warn("检查近期提醒失败: {}", e.getMessage());*/
return false;
} }
} }
/* *//**
* 每小时检查一次可选用于及时更新状态
*//*
@Scheduled(cron = "0 0 * * * ?")
public void hourlyCertificateCheck() {
log.debug("执行每小时证书状态检查...");
/**
* 生成预期的通知标题
*/
private String generateExpectedTitle(VetCertificate certificate, long daysRemaining) {
if (daysRemaining <= 0) {
long daysExpired = -daysRemaining;
if (daysExpired == 0) {
return "🚨 证书今天过期!";
} else {
return "🚨 证书已过期" + daysExpired + "天";
}
} else if (daysRemaining <= 7) {
return "⚠️ 证书还剩" + daysRemaining + "天过期";
} else if (daysRemaining == 15 || daysRemaining == 30) {
return "📅 证书还剩" + daysRemaining + "天过期";
} else {
return ""; // 其他情况不发送
}
}
/**
* 更新证书状态
*/
private void updateCertificateStatus(VetCertificate certificate, long daysRemaining) {
try { try {
// 更新所有证书状态
vetCertificateService.selectVetCertificateList(new VetCertificate())
.forEach(cert -> {
// 这里可以调用更新状态的方法
});
String newStatus = daysRemaining <= 0 ? "2" :
daysRemaining <= 30 ? "1" : "0";
if (!newStatus.equals(certificate.getStatus())) {
certificate.setStatus(newStatus);
/*log.debug("更新证书状态: 证书ID={}, 新状态={}", certificate.getId(), newStatus);*/
}
} catch (Exception e) { } catch (Exception e) {
log.error("每小时证书检查失败", e);
/*log.warn("更新证书状态失败: {}", e.getMessage());*/
} }
}*/
}
/**
* 计算天数差忽略时间部分
*/
private long calculateDayDifference(Date startDate, Date endDate) {
Calendar startCal = Calendar.getInstance();
startCal.setTime(startDate);
resetTime(startCal);
Calendar endCal = Calendar.getInstance();
endCal.setTime(endDate);
resetTime(endCal);
return (endCal.getTimeInMillis() - startCal.getTimeInMillis()) / (1000 * 60 * 60 * 24);
}
/**
* 重置时间为00:00:00
*/
private void resetTime(Calendar cal) {
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
}
} }

117
chenhai-system/src/main/java/com/chenhai/vet/domain/VetKnowledge.java

@ -0,0 +1,117 @@
package com.chenhai.vet.domain;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.chenhai.common.annotation.Excel;
import com.chenhai.common.core.domain.BaseEntity;
/**
* 兽医文章对象 vet_knowledge
*
* @author ruoyi
* @date 2026-01-08
*/
public class VetKnowledge extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键 */
private Long id;
/** 文章标题 */
@Excel(name = "文章标题")
private String title;
/** 文章内容 */
@Excel(name = "文章内容")
private String content;
/** 文章分类(治疗防治/饲养管理/其他) */
@Excel(name = "文章分类", readConverterExp = "治=疗防治/饲养管理/其他")
private String category;
/** 检测出的敏感词 */
@Excel(name = "检测出的敏感词")
private String sensitiveWords;
/** 状态(0-待审核 1-已发布 2-敏感内容驳回) */
@Excel(name = "状态", readConverterExp = "0=-待审核,1=-已发布,2=-敏感内容驳回")
private String status;
public void setId(Long id)
{
this.id = id;
}
public Long getId()
{
return id;
}
public void setTitle(String title)
{
this.title = title;
}
public String getTitle()
{
return title;
}
public void setContent(String content)
{
this.content = content;
}
public String getContent()
{
return content;
}
public void setCategory(String category)
{
this.category = category;
}
public String getCategory()
{
return category;
}
public void setSensitiveWords(String sensitiveWords)
{
this.sensitiveWords = sensitiveWords;
}
public String getSensitiveWords()
{
return sensitiveWords;
}
public void setStatus(String status)
{
this.status = status;
}
public String getStatus()
{
return status;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("title", getTitle())
.append("content", getContent())
.append("category", getCategory())
.append("sensitiveWords", getSensitiveWords())
.append("status", getStatus())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
}
}

2
chenhai-system/src/main/java/com/chenhai/vet/domain/VetNotification.java

@ -33,7 +33,7 @@ public class VetNotification extends BaseEntity
private String content; private String content;
/** 通知类型 */ /** 通知类型 */
@Excel(name = "通知类型")
@Excel(name = "通知类型" , dictType = "notification_type")
private String type; private String type;
/** 关联ID */ /** 关联ID */

188
chenhai-system/src/main/java/com/chenhai/vet/domain/VetQualification.java

@ -3,14 +3,19 @@ package com.chenhai.vet.domain;
import com.chenhai.common.annotation.Excel; import com.chenhai.common.annotation.Excel;
import com.chenhai.common.core.domain.BaseEntity; import com.chenhai.common.core.domain.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/** /**
* 兽医资质对象 vet_qualification * 兽医资质对象 vet_qualification
*
*
* @author ruoyi * @author ruoyi
* @date 2025-12-26 * @date 2025-12-26
*/ */
@ -23,7 +28,7 @@ public class VetQualification extends BaseEntity
/** 兽医ID */ /** 兽医ID */
@Excel(name = "兽医ID") @Excel(name = "兽医ID")
private Long vetId;
private Long userId;
/** 真实姓名 */ /** 真实姓名 */
@Excel(name = "真实姓名") @Excel(name = "真实姓名")
@ -33,8 +38,8 @@ public class VetQualification extends BaseEntity
@Excel(name = "身份证号") @Excel(name = "身份证号")
private String idCard; private String idCard;
/** 资质类型(veterinarian兽医资格证/pharmacist兽药师资格证) */
@Excel(name = "资质类型", readConverterExp = "v=eterinarian兽医资格证/pharmacist兽药师资格证")
/** 资质类型 */
@Excel(name = "资质类型", dictType = "qualification_type")
private String qualificationType; private String qualificationType;
/** 证书编号 */ /** 证书编号 */
@ -56,7 +61,7 @@ public class VetQualification extends BaseEntity
private Date auditTime; private Date auditTime;
/** 审核状态(0待审核 1通过 2拒绝) */ /** 审核状态(0待审核 1通过 2拒绝) */
@Excel(name = "审核状态", readConverterExp = "0=待审核,1=通过,2=拒绝")
@Excel(name = "审核状态", dictType = "qualification_shenhe")
private String auditStatus; private String auditStatus;
/** 审核意见 */ /** 审核意见 */
@ -67,142 +72,179 @@ public class VetQualification extends BaseEntity
@Excel(name = "审核人ID") @Excel(name = "审核人ID")
private Long auditorId; private Long auditorId;
/** 经营范围ID列表(逗号分隔) */
@Excel(name = "经营范围ID")
/** 经营范围IDs(存储到数据库的逗号分隔字符串) */
private String scopeIds; private String scopeIds;
/** 经营范围名称列表(逗号分隔) */ /** 经营范围名称列表(逗号分隔) */
@Excel(name = "经营范围名称")
@Excel(name = "经营范围名称", dictType = "scope_names")
private String scopeNames; private String scopeNames;
public void setQualificationId(Long qualificationId)
private String qualificationTypeLabel;
private String auditStatusLabel;
private String scopeNamesLabel;
// JSON 反序列化时使用的方法
@JsonProperty("scopeIds")
public void setScopeIdsFromJson(List<String> scopeIdList) {
// 自动转换为逗号分隔的字符串
if (scopeIdList != null && !scopeIdList.isEmpty()) {
this.scopeIds = String.join(",", scopeIdList);
} else {
this.scopeIds = null;
}
}
// 序列化为 JSON 时使用的方法
@JsonProperty("scopeIds")
public List<String> getScopeIdsAsList() {
if (scopeIds != null && !scopeIds.trim().isEmpty()) {
return new ArrayList<>(Arrays.asList(scopeIds.split(",")));
}
return new ArrayList<>();
}
// 数据库操作的 getterJackson 忽略
@JsonIgnore
public String getScopeIds() {
return scopeIds;
}
// 数据库操作的 setter
public void setScopeIds(String scopeIds) {
this.scopeIds = scopeIds;
}
// 为了方便添加一个获取列表的方法不映射到 JSON
@JsonIgnore
public List<String> getScopeIdList() {
return getScopeIdsAsList();
}
// 为了方便添加一个设置列表的方法不映射到 JSON
public void setScopeIdList(List<String> scopeIdList) {
setScopeIdsFromJson(scopeIdList);
}
public void setQualificationId(Long qualificationId)
{ {
this.qualificationId = qualificationId; this.qualificationId = qualificationId;
} }
public Long getQualificationId()
public Long getQualificationId()
{ {
return qualificationId; return qualificationId;
} }
public void setVetId(Long vetId)
public void setUserId(Long userId)
{ {
this.vetId = vetId;
this.userId = userId;
} }
public Long getVetId()
public Long getUserId()
{ {
return vetId;
return userId;
} }
public void setRealName(String realName)
public void setRealName(String realName)
{ {
this.realName = realName; this.realName = realName;
} }
public String getRealName()
public String getRealName()
{ {
return realName; return realName;
} }
public void setIdCard(String idCard)
public void setIdCard(String idCard)
{ {
this.idCard = idCard; this.idCard = idCard;
} }
public String getIdCard()
public String getIdCard()
{ {
return idCard; return idCard;
} }
public void setQualificationType(String qualificationType)
public void setQualificationType(String qualificationType)
{ {
this.qualificationType = qualificationType; this.qualificationType = qualificationType;
} }
public String getQualificationType()
public String getQualificationType()
{ {
return qualificationType; return qualificationType;
} }
public void setCertificateNo(String certificateNo)
public void setCertificateNo(String certificateNo)
{ {
this.certificateNo = certificateNo; this.certificateNo = certificateNo;
} }
public String getCertificateNo()
public String getCertificateNo()
{ {
return certificateNo; return certificateNo;
} }
public void setCertificateFiles(String certificateFiles)
public void setCertificateFiles(String certificateFiles)
{ {
this.certificateFiles = certificateFiles; this.certificateFiles = certificateFiles;
} }
public String getCertificateFiles()
public String getCertificateFiles()
{ {
return certificateFiles; return certificateFiles;
} }
public void setApplyTime(Date applyTime)
public void setApplyTime(Date applyTime)
{ {
this.applyTime = applyTime; this.applyTime = applyTime;
} }
public Date getApplyTime()
public Date getApplyTime()
{ {
return applyTime; return applyTime;
} }
public void setAuditTime(Date auditTime)
public void setAuditTime(Date auditTime)
{ {
this.auditTime = auditTime; this.auditTime = auditTime;
} }
public Date getAuditTime()
public Date getAuditTime()
{ {
return auditTime; return auditTime;
} }
public void setAuditStatus(String auditStatus)
public void setAuditStatus(String auditStatus)
{ {
this.auditStatus = auditStatus; this.auditStatus = auditStatus;
} }
public String getAuditStatus()
public String getAuditStatus()
{ {
return auditStatus; return auditStatus;
} }
public void setAuditOpinion(String auditOpinion)
public void setAuditOpinion(String auditOpinion)
{ {
this.auditOpinion = auditOpinion; this.auditOpinion = auditOpinion;
} }
public String getAuditOpinion()
public String getAuditOpinion()
{ {
return auditOpinion; return auditOpinion;
} }
public void setAuditorId(Long auditorId)
public void setAuditorId(Long auditorId)
{ {
this.auditorId = auditorId; this.auditorId = auditorId;
} }
public Long getAuditorId()
public Long getAuditorId()
{ {
return auditorId; return auditorId;
} }
public String getScopeIds() {
return scopeIds;
}
public void setScopeIds(String scopeIds) {
this.scopeIds = scopeIds;
}
public String getScopeNames() { public String getScopeNames() {
return scopeNames; return scopeNames;
} }
@ -211,28 +253,52 @@ public class VetQualification extends BaseEntity
this.scopeNames = scopeNames; this.scopeNames = scopeNames;
} }
public String getQualificationTypeLabel() {
return qualificationTypeLabel;
}
public void setQualificationTypeLabel(String qualificationTypeLabel) {
this.qualificationTypeLabel = qualificationTypeLabel;
}
public String getAuditStatusLabel() {
return auditStatusLabel;
}
public void setAuditStatusLabel(String auditStatusLabel) {
this.auditStatusLabel = auditStatusLabel;
}
public String getScopeNamesLabel() {
return scopeNamesLabel;
}
public void setScopeNamesLabel(String scopeNamesLabel) {
this.scopeNamesLabel = scopeNamesLabel;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("qualificationId", getQualificationId())
.append("vetId", getVetId())
.append("realName", getRealName())
.append("idCard", getIdCard())
.append("qualificationType", getQualificationType())
.append("certificateNo", getCertificateNo())
.append("certificateFiles", getCertificateFiles())
.append("applyTime", getApplyTime())
.append("auditTime", getAuditTime())
.append("auditStatus", getAuditStatus())
.append("auditOpinion", getAuditOpinion())
.append("auditorId", getAuditorId())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("qualificationId", getQualificationId())
.append("userId", getUserId())
.append("realName", getRealName())
.append("idCard", getIdCard())
.append("qualificationType", getQualificationType())
.append("certificateNo", getCertificateNo())
.append("certificateFiles", getCertificateFiles())
.append("applyTime", getApplyTime())
.append("auditTime", getAuditTime())
.append("auditStatus", getAuditStatus())
.append("auditOpinion", getAuditOpinion())
.append("auditorId", getAuditorId())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("scopeIds", getScopeIds()) .append("scopeIds", getScopeIds())
.append("scopeNames", getScopeNames()) .append("scopeNames", getScopeNames())
.toString();
.toString();
} }
}
}

26
chenhai-system/src/main/java/com/chenhai/vet/domain/VetTrainingVideo.java

@ -0,0 +1,26 @@
package com.chenhai.vet.domain;
import lombok.Data;
import java.util.Date;
@Data
public class VetTrainingVideo {
private Long id;
private Long userId;
private String title;
private String description;
private String videoUrl;
private String coverImage;
private String category;
private String tags;
private Integer duration; // 视频时长
private Long fileSize; // 文件大小字节
private Integer viewCount;
private String status; // 0-私有 1-公开
private Date createTime;
private Date updateTime;
// 非数据库字段
private String userName; // 兽医姓名
private String durationStr; // 格式化后的时长12:30
}

61
chenhai-system/src/main/java/com/chenhai/vet/mapper/VetKnowledgeMapper.java

@ -0,0 +1,61 @@
package com.chenhai.vet.mapper;
import java.util.List;
import com.chenhai.vet.domain.VetKnowledge;
/**
* 兽医文章Mapper接口
*
* @author ruoyi
* @date 2026-01-08
*/
public interface VetKnowledgeMapper
{
/**
* 查询兽医文章
*
* @param id 兽医文章主键
* @return 兽医文章
*/
public VetKnowledge selectVetKnowledgeById(Long id);
/**
* 查询兽医文章列表
*
* @param vetKnowledge 兽医文章
* @return 兽医文章集合
*/
public List<VetKnowledge> selectVetKnowledgeList(VetKnowledge vetKnowledge);
/**
* 新增兽医文章
*
* @param vetKnowledge 兽医文章
* @return 结果
*/
public int insertVetKnowledge(VetKnowledge vetKnowledge);
/**
* 修改兽医文章
*
* @param vetKnowledge 兽医文章
* @return 结果
*/
public int updateVetKnowledge(VetKnowledge vetKnowledge);
/**
* 删除兽医文章
*
* @param id 兽医文章主键
* @return 结果
*/
public int deleteVetKnowledgeById(Long id);
/**
* 批量删除兽医文章
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteVetKnowledgeByIds(Long[] ids);
}

29
chenhai-system/src/main/java/com/chenhai/vet/mapper/VetNotificationMapper.java

@ -4,7 +4,10 @@ package com.chenhai.vet.mapper;
import com.chenhai.vet.domain.VetNotification; import com.chenhai.vet.domain.VetNotification;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 兽医通知Mapper接口 * 兽医通知Mapper接口
@ -94,14 +97,6 @@ public interface VetNotificationMapper
*/ */
public int markAllNotificationsAsRead(Long userId); public int markAllNotificationsAsRead(Long userId);
/**
* 查询用户未读通知数量
*
* @param userId 用户ID
* @return 未读通知数量
*/
public Integer countUnreadNotificationsByUserId(Long userId);
/** /**
* 批量插入通知 * 批量插入通知
* *
@ -110,13 +105,21 @@ public interface VetNotificationMapper
*/ */
public int batchInsertVetNotifications(@Param("list") List<VetNotification> notifications); public int batchInsertVetNotifications(@Param("list") List<VetNotification> notifications);
List<VetNotification> selectVetNotificationByUserId(Long userId);
/** /**
* 清理过期通知
* 获取用户通知统计已读和未读数量
* *
* @param days 过期天数
* @return 清理数量
* @param userId 用户ID
* @return 统计结果
*/ */
public int cleanExpiredNotifications(@Param("days") Integer days);
public Map<String, Object> selectNotificationStatsByUserId(Long userId);
List<VetNotification> selectNotificationsByDate(
@Param("userId") Long userId,
@Param("certificateId") Long certificateId,
@Param("date") Date date
);
List<VetNotification> selectVetNotificationByUserId(Long userId);
} }

3
chenhai-system/src/main/java/com/chenhai/vet/mapper/VetQualificationMapper.java

@ -2,6 +2,7 @@ package com.chenhai.vet.mapper;
import com.chenhai.vet.domain.VetQualification; import com.chenhai.vet.domain.VetQualification;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import java.util.List; import java.util.List;
@ -62,5 +63,5 @@ public interface VetQualificationMapper
*/ */
public int deleteVetQualificationByQualificationIds(Long[] qualificationIds); public int deleteVetQualificationByQualificationIds(Long[] qualificationIds);
public int submitAudit(Long qualificationId);
public int submit(Long qualificationId);
} }

28
chenhai-system/src/main/java/com/chenhai/vet/mapper/VetTrainingVideoMapper.java

@ -0,0 +1,28 @@
package com.chenhai.vet.mapper;
import com.chenhai.vet.domain.VetTrainingVideo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface VetTrainingVideoMapper {
int insertVideo(VetTrainingVideo video);
List<VetTrainingVideo> selectMyVideos(@Param("userId") Long userId,
@Param("title") String title,
@Param("category") String category,
@Param("status") String status);
List<VetTrainingVideo> selectPublicVideos(@Param("title") String title,
@Param("category") String category,
@Param("userName") String userName);
VetTrainingVideo selectVideoById(Long id);
void incrementViewCount(Long id);
List<VetTrainingVideo> selectHotVideos(@Param("limit") Integer limit);
List<VetTrainingVideo> searchVideos(@Param("keyword") String keyword);
int deleteVideoById(@Param("id") Long id);
}

6
chenhai-system/src/main/java/com/chenhai/vet/service/IVetCertificateService.java

@ -63,4 +63,10 @@ public interface IVetCertificateService
* 获取证书统计信息 * 获取证书统计信息
*/ */
Map<String, Object> getCertificateStatistics(Long userId); Map<String, Object> getCertificateStatistics(Long userId);
} }

74
chenhai-system/src/main/java/com/chenhai/vet/service/IVetKnowledgeService.java

@ -0,0 +1,74 @@
package com.chenhai.vet.service;
import java.util.List;
import com.chenhai.common.core.domain.AjaxResult;
import com.chenhai.vet.domain.VetKnowledge;
/**
* 兽医文章Service接口
*
* @author ruoyi
* @date 2026-01-08
*/
public interface IVetKnowledgeService
{
/**
* 查询兽医文章
*
* @param id 兽医文章主键
* @return 兽医文章
*/
public VetKnowledge selectVetKnowledgeById(Long id);
/**
* 查询兽医文章列表
*
* @param vetKnowledge 兽医文章
* @return 兽医文章集合
*/
public List<VetKnowledge> selectVetKnowledgeList(VetKnowledge vetKnowledge);
/**
* 新增兽医文章
*
* @param vetKnowledge 兽医文章
* @return 结果
*/
public int insertVetKnowledge(VetKnowledge vetKnowledge);
/**
* 修改兽医文章
*
* @param vetKnowledge 兽医文章
* @return 结果
*/
public int updateVetKnowledge(VetKnowledge vetKnowledge);
/**
* 批量删除兽医文章
*
* @param ids 需要删除的兽医文章主键集合
* @return 结果
*/
public int deleteVetKnowledgeByIds(Long[] ids);
/**
* 删除兽医文章信息
*
* @param id 兽医文章主键
* @return 结果
*/
public int deleteVetKnowledgeById(Long id);
/**
* 上传文章待审核
*/
AjaxResult uploadVetKnowledge(VetKnowledge vetKnowledge);
/**
* 发布文章更新状态为已发布
*/
AjaxResult publishVetKnowledge(Long id);
}

1
chenhai-system/src/main/java/com/chenhai/vet/service/IVetQualificationService.java

@ -60,5 +60,4 @@ public interface IVetQualificationService
*/ */
public int deleteVetQualificationByQualificationId(Long qualificationId); public int deleteVetQualificationByQualificationId(Long qualificationId);
public int submitAudit(Long qualificationId);
} }

51
chenhai-system/src/main/java/com/chenhai/vet/service/IVetTrainingVideoService.java

@ -0,0 +1,51 @@
package com.chenhai.vet.service;
import com.chenhai.vet.domain.VetTrainingVideo;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface IVetTrainingVideoService {
/**
* 上传并保存视频
*/
String uploadAndSave(VetTrainingVideo video, MultipartFile videoFile, MultipartFile coverImage);
/**
* 获取我的视频列表
*/
List<VetTrainingVideo> getMyVideos(Long userId, String title, String category, String status);
/**
* 获取公开视频列表
*/
List<VetTrainingVideo> getPublicVideos(String title, String category, String vetName);
/**
* 获取视频详情带权限校验
*/
VetTrainingVideo getVideoDetail(Long videoId, Long currentVetId);
/**
* 获取视频播放地址带权限校验
*/
String getVideoPlayUrl(Long videoId, Long currentVetId);
/**
* 增加观看次数
*/
void incrementViewCount(Long videoId);
/**
* 获取热门视频
*/
List<VetTrainingVideo> getHotVideos(Integer limit);
/**
* 搜索视频
*/
List<VetTrainingVideo> searchVideos(String keyword);
int deleteVideoById(Long videoId);
}

15
chenhai-system/src/main/java/com/chenhai/vet/service/VetNotificationService.java

@ -4,6 +4,7 @@ package com.chenhai.vet.service;
import com.chenhai.vet.domain.VetCertificate; import com.chenhai.vet.domain.VetCertificate;
import com.chenhai.vet.domain.VetNotification; import com.chenhai.vet.domain.VetNotification;
import java.util.List; import java.util.List;
import java.util.Map;
public interface VetNotificationService { public interface VetNotificationService {
@ -42,11 +43,6 @@ public interface VetNotificationService {
*/ */
List<VetNotification> getNotificationsByUserId(Long userId); List<VetNotification> getNotificationsByUserId(Long userId);
/**
* 获取未读通知数量
*/
int getUnreadCount(Long userId);
/** /**
* 发送证书过期提醒通知 * 发送证书过期提醒通知
*/ */
@ -57,10 +53,9 @@ public interface VetNotificationService {
*/ */
boolean markAsRead(Long notificationId); boolean markAsRead(Long notificationId);
/**
* 标记所有通知为已读
*/
boolean markAllAsRead(Long userId);
List<VetNotification> selectByUserId(Long userId); List<VetNotification> selectByUserId(Long userId);
Map<String, Integer> getNotificationStats(Long userId);
} }

2
chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetCertificateServiceImpl.java

@ -344,4 +344,6 @@ public class VetCertificateServiceImpl implements IVetCertificateService
return statistics; return statistics;
} }
} }

134
chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetKnowledgeServiceImpl.java

@ -0,0 +1,134 @@
package com.chenhai.vet.service.impl;
import java.util.List;
import com.chenhai.common.core.domain.AjaxResult;
import com.chenhai.common.utils.DateUtils;
import com.chenhai.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.chenhai.vet.mapper.VetKnowledgeMapper;
import com.chenhai.vet.domain.VetKnowledge;
import com.chenhai.vet.service.IVetKnowledgeService;
/**
* 兽医文章Service业务层处理
*
* @author ruoyi
* @date 2026-01-08
*/
@Service
public class VetKnowledgeServiceImpl implements IVetKnowledgeService
{
@Autowired
private VetKnowledgeMapper vetKnowledgeMapper;
/**
* 查询兽医文章
*
* @param id 兽医文章主键
* @return 兽医文章
*/
@Override
public VetKnowledge selectVetKnowledgeById(Long id)
{
return vetKnowledgeMapper.selectVetKnowledgeById(id);
}
/**
* 查询兽医文章列表
*
* @param vetKnowledge 兽医文章
* @return 兽医文章
*/
@Override
public List<VetKnowledge> selectVetKnowledgeList(VetKnowledge vetKnowledge)
{
return vetKnowledgeMapper.selectVetKnowledgeList(vetKnowledge);
}
/**
* 新增兽医文章
*
* @param vetKnowledge 兽医文章
* @return 结果
*/
@Override
public int insertVetKnowledge(VetKnowledge vetKnowledge)
{
vetKnowledge.setCreateTime(DateUtils.getNowDate());
return vetKnowledgeMapper.insertVetKnowledge(vetKnowledge);
}
/**
* 修改兽医文章
*
* @param vetKnowledge 兽医文章
* @return 结果
*/
@Override
public int updateVetKnowledge(VetKnowledge vetKnowledge)
{
vetKnowledge.setUpdateTime(DateUtils.getNowDate());
return vetKnowledgeMapper.updateVetKnowledge(vetKnowledge);
}
/**
* 批量删除兽医文章
*
* @param ids 需要删除的兽医文章主键
* @return 结果
*/
@Override
public int deleteVetKnowledgeByIds(Long[] ids)
{
return vetKnowledgeMapper.deleteVetKnowledgeByIds(ids);
}
/**
* 删除兽医文章信息
*
* @param id 兽医文章主键
* @return 结果
*/
@Override
public int deleteVetKnowledgeById(Long id)
{
return vetKnowledgeMapper.deleteVetKnowledgeById(id);
}
/**
* 上传文章默认状态为0-待审核
*/
@Override
public AjaxResult uploadVetKnowledge(VetKnowledge vetKnowledge) {
// 填充基础信息
vetKnowledge.setCreateBy(SecurityUtils.getUsername());
vetKnowledge.setCreateTime(DateUtils.getNowDate());
vetKnowledge.setStatus("0"); // 固定为待审核
int result = vetKnowledgeMapper.insertVetKnowledge(vetKnowledge);
if (result > 0) {
return AjaxResult.success("文章上传成功,等待审核");
}
return AjaxResult.error("文章上传失败");
}
/**
* 发布文章更新状态为1-已发布
*/
@Override
public AjaxResult publishVetKnowledge(Long id) {
VetKnowledge vetKnowledge = new VetKnowledge();
vetKnowledge.setId(id);
vetKnowledge.setStatus("1"); // 已发布
vetKnowledge.setUpdateBy(SecurityUtils.getUsername());
vetKnowledge.setUpdateTime(DateUtils.getNowDate());
int result = vetKnowledgeMapper.updateVetKnowledge(vetKnowledge);
if (result > 0) {
return AjaxResult.success("文章发布成功");
}
return AjaxResult.error("文章发布失败");
}
}

78
chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetNotificationServiceImpl.java

@ -13,6 +13,7 @@ import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -59,57 +60,14 @@ public class VetNotificationServiceImpl implements VetNotificationService {
} }
@Override @Override
public int getUnreadCount(Long userId) {
VetNotification query = new VetNotification();
query.setUserId(userId);
query.setIsRead(0);
List<VetNotification> list = vetNotificationMapper.selectVetNotificationList(query);
return list != null ? list.size() : 0;
public void sendCertificateExpireRemind(VetCertificate certificate) {
log.info("证书提醒记录: 证书ID={}, 名称={}, 过期时间={}",
certificate.getId(), certificate.getCertName(),
formatDate(certificate.getExpireDate()));
} }
@Override
public void sendCertificateExpireRemind(VetCertificate certificate) {
if (certificate == null || certificate.getExpireDate() == null) {
return;
}
Date now = new Date();
Date expireDate = certificate.getExpireDate();
// 计算距离过期的天数
long daysBetween = calculateDayDifference(now, expireDate);
VetNotification notification = new VetNotification();
notification.setUserId(certificate.getUserId());
notification.setRelatedId(certificate.getId().toString());
notification.setType("CERT_EXPIRE_REMIND");
notification.setIsRead(0);
notification.setCreateTime(now); // 直接使用 Date
if (daysBetween <= 0) {
notification.setTitle(" ⚠️ 证书已过期");
notification.setContent(String.format("您的《%s》证书已于%s过期!请立即更新证书以继续提供服务。",
certificate.getCertName(), formatDate(expireDate)));
notification.setRemindLevel(3);
} else if (daysBetween <= 7) {
notification.setTitle("⚠️ 证书即将过期(7天内)");
notification.setContent(String.format("您的《%s》证书将在%d天后过期,请尽快完成更新!",
certificate.getCertName(), daysBetween));
notification.setRemindLevel(3);
} else if (daysBetween <= 30) {
notification.setTitle("⚠️ 证书即将过期(30天内)");
notification.setContent(String.format("您的《%s》证书将在%d天后过期,请及时安排更新。",
certificate.getCertName(), daysBetween));
notification.setRemindLevel(2);
} else {
// 超过30天不发送常规提醒
return;
}
vetNotificationMapper.insertVetNotification(notification);
log.info("发送证书提醒通知:用户ID={}, 证书ID={}, 天数={}",
certificate.getUserId(), certificate.getId(), daysBetween);
}
@Override @Override
public boolean markAsRead(Long notificationId) { public boolean markAsRead(Long notificationId) {
@ -123,21 +81,6 @@ public class VetNotificationServiceImpl implements VetNotificationService {
return false; return false;
} }
@Override
public boolean markAllAsRead(Long userId) {
List<VetNotification> unreadNotifications = getNotificationsByUserId(userId);
int count = 0;
for (VetNotification notification : unreadNotifications) {
if (notification.getIsRead() == 0) {
notification.setIsRead(1);
notification.setReadTime(new Date());
vetNotificationMapper.updateVetNotification(notification);
count++;
}
}
log.info("标记所有通知为已读:用户ID={}, 标记数量={}", userId, count);
return count > 0;
}
/** /**
* 计算两个日期之间的天数差忽略时间部分 * 计算两个日期之间的天数差忽略时间部分
@ -180,4 +123,15 @@ public class VetNotificationServiceImpl implements VetNotificationService {
public List<VetNotification> selectByUserId(Long userId) { public List<VetNotification> selectByUserId(Long userId) {
return vetNotificationMapper.selectVetNotificationByUserId(userId); return vetNotificationMapper.selectVetNotificationByUserId(userId);
} }
@Override
public Map<String, Integer> getNotificationStats(Long userId) {
// 查询统计信息
Map<String, Object> stats = vetNotificationMapper.selectNotificationStatsByUserId(userId);
return Map.of(
"unread", ((Number) stats.getOrDefault("unread", 0)).intValue(),
"read", ((Number) stats.getOrDefault("read", 0)).intValue()
);
}
} }

188
chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetQualificationServiceImpl.java

@ -1,30 +1,24 @@
package com.chenhai.vet.service.impl; package com.chenhai.vet.service.impl;
import com.chenhai.common.utils.DateUtils;
import com.chenhai.common.utils.StringUtils;
import com.chenhai.vet.domain.VetQualification; import com.chenhai.vet.domain.VetQualification;
import com.chenhai.vet.mapper.VetQualificationMapper; import com.chenhai.vet.mapper.VetQualificationMapper;
import com.chenhai.vet.service.IVetQualificationService; import com.chenhai.vet.service.IVetQualificationService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import com.chenhai.vet.domain.BusinessScopeConstants;
/**
* 兽医资质Service业务层处理
*
* @author ruoyi
* @date 2025-12-26
*/
@Service @Service
public class VetQualificationServiceImpl implements IVetQualificationService
{
public class VetQualificationServiceImpl implements IVetQualificationService {
@Autowired @Autowired
private VetQualificationMapper vetQualificationMapper; private VetQualificationMapper vetQualificationMapper;
// 修复2给JdbcTemplate加注入注解
@Autowired
private JdbcTemplate jdbcTemplate;
/** /**
@ -33,79 +27,85 @@ public class VetQualificationServiceImpl implements IVetQualificationService
@Override @Override
public List<VetQualification> selectVetQualificationList(VetQualification vetQualification) { public List<VetQualification> selectVetQualificationList(VetQualification vetQualification) {
List<VetQualification> list = vetQualificationMapper.selectVetQualificationList(vetQualification); List<VetQualification> list = vetQualificationMapper.selectVetQualificationList(vetQualification);
// 处理经营范围名称如果数据库中没有存储scope_names
// 处理经营范围名称scope_ids转scope_names
for (VetQualification qualification : list) { for (VetQualification qualification : list) {
if (qualification.getScopeIds() != null && !qualification.getScopeIds().isEmpty()
&& (qualification.getScopeNames() == null || qualification.getScopeNames().isEmpty())) {
// 处理经营范围名称
processScopeNames(qualification);
if ((qualification.getScopeNames() == null || qualification.getScopeNames().isEmpty())
&& qualification.getScopeIds() != null && !qualification.getScopeIds().isEmpty()) {
String scopeNames = getScopeNamesFromDict(qualification.getScopeIds());
qualification.setScopeNames(scopeNames);
} }
} }
return list; return list;
} }
/** /**
* 处理经营范围名称
* 从字典表根据scope_ids获取scope_names
*/ */
private void processScopeNames(VetQualification qualification) {
if (qualification.getScopeIds() != null && !qualification.getScopeIds().isEmpty()) {
String[] scopeIds = qualification.getScopeIds().split(",");
List<String> scopeNames = new ArrayList<>();
for (String scopeId : scopeIds) {
String name = BusinessScopeConstants.getScopeName(scopeId.trim());
if (name != null) {
scopeNames.add(name);
}
}
if (!scopeNames.isEmpty()) {
qualification.setScopeNames(String.join(",", scopeNames));
private String getScopeNamesFromDict(String scopeIds) {
if (scopeIds == null || scopeIds.isEmpty()) {
return "";
}
String[] idArray = scopeIds.split(",");
List<String> cleanedIds = new ArrayList<>();
for (String id : idArray) {
cleanedIds.add("'" + id.trim() + "'");
}
String inCondition = String.join(",", cleanedIds);
String sql = "SELECT dict_label FROM sys_dict_data " +
"WHERE dict_type = 'business_scope' " +
"AND dict_value IN (" + inCondition + ") " +
"AND status = '0' " +
"ORDER BY FIND_IN_SET(dict_value, ?)";
List<String> names = jdbcTemplate.query(sql,
(rs, rowNum) -> rs.getString("dict_label"),
scopeIds);
return String.join(",", names);
}
/**
* 处理资质文件路径
*/
private String processCertificateFiles(String certificateFilesJson) {
try {
if (StringUtils.isEmpty(certificateFilesJson)) {
return "";
} }
// 如果certificateFiles是JSON数组字符串这里可以根据需要处理
// 简单处理直接存储为字符串或解析JSON后存储
return certificateFilesJson;
} catch (Exception e) {
return certificateFilesJson;
} }
} }
/** /**
* 查询兽医资质 * 查询兽医资质
*
* @param qualificationId 兽医资质主键
* @return 兽医资质
*/ */
@Override @Override
public VetQualification selectVetQualificationByQualificationId(Long qualificationId)
{
public VetQualification selectVetQualificationByQualificationId(Long qualificationId) {
return vetQualificationMapper.selectVetQualificationByQualificationId(qualificationId); return vetQualificationMapper.selectVetQualificationByQualificationId(qualificationId);
} }
/**
* 查询兽医资质列表
*
* @param vetQualification 兽医资质
* @return 兽医资质
*/
/*@Override
public List<VetQualification> selectVetQualificationList(VetQualification vetQualification)
{
return vetQualificationMapper.selectVetQualificationList(vetQualification);
}
*/
/** /**
* 新增兽医资质 * 新增兽医资质
*
* @param vetQualification 兽医资质
* @return 结果
*/ */
@Override @Override
public int insertVetQualification(VetQualification vetQualification)
{
// 确保审核状态默认为"2"未提交
public int insertVetQualification(VetQualification vetQualification) {
if (vetQualification.getAuditStatus() == null) { if (vetQualification.getAuditStatus() == null) {
vetQualification.setAuditStatus("2"); vetQualification.setAuditStatus("2");
} }
if (vetQualification.getCertificateFiles() == null ||
vetQualification.getCertificateFiles().isEmpty()) {
vetQualification.setCertificateFiles("default.pdf"); // 默认值
}
// 如果传入了scopeIds但未设置scopeNames自动处理
if (vetQualification.getScopeIds() != null && !vetQualification.getScopeIds().isEmpty()
&& (vetQualification.getScopeNames() == null || vetQualification.getScopeNames().isEmpty())) {
processScopeNames(vetQualification);
if (vetQualification.getScopeIds() != null && !vetQualification.getScopeIds().isEmpty()) {
String scopeNames = getScopeNamesFromDict(vetQualification.getScopeIds());
vetQualification.setScopeNames(scopeNames);
} }
return vetQualificationMapper.insertVetQualification(vetQualification); return vetQualificationMapper.insertVetQualification(vetQualification);
@ -113,47 +113,71 @@ public class VetQualificationServiceImpl implements IVetQualificationService
/** /**
* 修改兽医资质 * 修改兽医资质
*
* @param vetQualification 兽医资质
* @return 结果
*/ */
@Override @Override
public int updateVetQualification(VetQualification vetQualification)
{
vetQualification.setUpdateTime(DateUtils.getNowDate());
public int updateVetQualification(VetQualification vetQualification) {
// 处理资质文件路径
if (vetQualification.getCertificateFiles() != null) {
vetQualification.setCertificateFiles(processCertificateFiles(vetQualification.getCertificateFiles()));
}
if (vetQualification.getScopeIds() != null) {
String scopeNames = getScopeNamesFromDict(vetQualification.getScopeIds());
vetQualification.setScopeNames(scopeNames);
}
return vetQualificationMapper.updateVetQualification(vetQualification); return vetQualificationMapper.updateVetQualification(vetQualification);
} }
/** /**
* 批量删除兽医资质 * 批量删除兽医资质
*
* @param qualificationIds 需要删除的兽医资质主键
* @return 结果
*/ */
@Override @Override
public int deleteVetQualificationByQualificationIds(Long[] qualificationIds)
{
public int deleteVetQualificationByQualificationIds(Long[] qualificationIds) {
return vetQualificationMapper.deleteVetQualificationByQualificationIds(qualificationIds); return vetQualificationMapper.deleteVetQualificationByQualificationIds(qualificationIds);
} }
/** /**
* 删除兽医资质信息 * 删除兽医资质信息
*
* @param qualificationId 兽医资质主键
* @return 结果
*/ */
@Override @Override
public int deleteVetQualificationByQualificationId(Long qualificationId)
{
public int deleteVetQualificationByQualificationId(Long qualificationId) {
return vetQualificationMapper.deleteVetQualificationByQualificationId(qualificationId); return vetQualificationMapper.deleteVetQualificationByQualificationId(qualificationId);
} }
@Override
public int submitAudit(Long qualificationId) {
VetQualification vetQualification = new VetQualification();
vetQualification.setQualificationId(qualificationId);
vetQualification.setAuditStatus("0"); // 待审核
vetQualification.setApplyTime(new Date());
return vetQualificationMapper.updateVetQualification(vetQualification);
private void createCertificateFromQualification(VetQualification qualification) {
try {
// 检查是否已存在证书
String checkSql = "SELECT COUNT(*) FROM vet_certificate WHERE qualification_id = ?";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class,
qualification.getQualificationId());
if (count != null && count > 0) {
System.out.println("资质ID " + qualification.getQualificationId() + " 已存在证书,跳过创建");
return;
}
String sql = "INSERT INTO vet_certificate (" +
"user_id, cert_number, " +
"create_by, create_time, " +
"qualification_id" +
") VALUES (?, ?, ?, NOW(), ?)";
jdbcTemplate.update(sql,
qualification.getUserId(),
qualification.getCertificateNo(),
qualification.getCreateBy(),
qualification.getQualificationId()
);
System.out.println("✅ 审核通过,已创建证书编号:" + qualification.getCertificateNo());
System.out.println("ℹ️ 证书名称、类型、发证机构等信息请在证书管理页面完善");
} catch (Exception e) {
System.err.println("❌ 创建证书失败:" + e.getMessage());
e.printStackTrace();
}
} }
} }

146
chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetTrainingVideoServiceImpl.java

@ -0,0 +1,146 @@
package com.chenhai.vet.service.impl;
import com.chenhai.vet.domain.VetTrainingVideo;
import com.chenhai.vet.mapper.VetTrainingVideoMapper;
import com.chenhai.vet.service.IVetTrainingVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Service
public class VetTrainingVideoServiceImpl implements IVetTrainingVideoService {
@Autowired
private VetTrainingVideoMapper videoMapper;
@Value("${file.upload.path:/uploads}")
private String uploadPath;
@Override
public String uploadAndSave(VetTrainingVideo video, MultipartFile videoFile, MultipartFile coverImage) {
try {
// 1. 创建上传目录
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// 2. 生成唯一文件名
String originalFileName = videoFile.getOriginalFilename();
String fileExtension = getFileExtension(originalFileName);
String uniqueFileName = UUID.randomUUID().toString() + "." + fileExtension;
// 3. 保存视频文件
Path videoPath = Paths.get(uploadPath, uniqueFileName);
Files.write(videoPath, videoFile.getBytes());
// 4. 保存封面图如果有
String coverImageUrl = null;
if (coverImage != null && !coverImage.isEmpty()) {
String coverExtension = getFileExtension(coverImage.getOriginalFilename());
String coverFileName = "cover_" + UUID.randomUUID().toString() + "." + coverExtension;
Path coverPath = Paths.get(uploadPath, coverFileName);
Files.write(coverPath, coverImage.getBytes());
coverImageUrl = "/uploads/" + coverFileName;
}
// 5. 计算视频时长和大小
int duration = getVideoDuration(videoFile); // 需要实现这个方法
long fileSize = videoFile.getSize();
// 6. 保存到数据库
video.setVideoUrl("/uploads/" + uniqueFileName);
video.setCoverImage(coverImageUrl);
video.setDuration(duration);
video.setFileSize(fileSize);
video.setViewCount(0);
video.setCreateTime(new Date());
video.setUpdateTime(new Date());
videoMapper.insertVideo(video);
return "上传成功!视频ID:" + video.getId();
} catch (IOException e) {
throw new RuntimeException("文件保存失败", e);
}
}
@Override
public List<VetTrainingVideo> getMyVideos(Long userId, String title, String category, String status) {
return videoMapper.selectMyVideos(userId, title, category, status);
}
@Override
public List<VetTrainingVideo> getPublicVideos(String title, String category, String vetName) {
return videoMapper.selectPublicVideos(title, category, vetName);
}
@Override
public VetTrainingVideo getVideoDetail(Long videoId, Long currentVetId) {
VetTrainingVideo video = videoMapper.selectVideoById(videoId);
if (video == null) {
return null;
}
// 权限校验只能查看公开视频或自己的视频
boolean canView = "1".equals(video.getStatus()) || currentVetId.equals(video.getUserId());
return canView ? video : null;
}
@Override
public String getVideoPlayUrl(Long videoId, Long currentVetId) {
VetTrainingVideo video = videoMapper.selectVideoById(videoId);
if (video == null) {
return null;
}
// 权限校验
boolean canPlay = "1".equals(video.getStatus()) || currentVetId.equals(video.getUserId());
return canPlay ? video.getVideoUrl() : null;
}
@Override
public void incrementViewCount(Long videoId) {
videoMapper.incrementViewCount(videoId);
}
@Override
public List<VetTrainingVideo> getHotVideos(Integer limit) {
return videoMapper.selectHotVideos(limit);
}
@Override
public List<VetTrainingVideo> searchVideos(String keyword) {
return videoMapper.searchVideos(keyword);
}
private String getFileExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
private int getVideoDuration(MultipartFile videoFile) {
// 这里需要实现获取视频时长的方法
// 可以使用 FFmpeg Java 的库来获取
// 暂时返回一个默认值
return 60; // 默认60秒
}
@Override
public int deleteVideoById(Long videoId) {
// 逻辑删除设置 del_flag = '1'
return videoMapper.deleteVideoById(videoId);
}
}

96
chenhai-system/src/main/resources/mapper/vet/VetKnowledgeMapper.xml

@ -0,0 +1,96 @@
<?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.chenhai.vet.mapper.VetKnowledgeMapper">
<resultMap type="VetKnowledge" id="VetKnowledgeResult">
<result property="id" column="id" />
<result property="title" column="title" />
<result property="content" column="content" />
<result property="category" column="category" />
<result property="sensitiveWords" column="sensitive_words" />
<result property="status" column="status" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectVetKnowledgeVo">
select id, title, content, category, sensitive_words, status, create_by, create_time, update_by, update_time, remark from vet_knowledge
</sql>
<select id="selectVetKnowledgeList" parameterType="VetKnowledge" resultMap="VetKnowledgeResult">
<include refid="selectVetKnowledgeVo"/>
<where>
<if test="title != null and title != ''"> and title = #{title}</if>
<if test="content != null and content != ''"> and content = #{content}</if>
<if test="category != null and category != ''"> and category = #{category}</if>
<if test="sensitiveWords != null and sensitiveWords != ''"> and sensitive_words = #{sensitiveWords}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
</select>
<select id="selectVetKnowledgeById" parameterType="Long" resultMap="VetKnowledgeResult">
<include refid="selectVetKnowledgeVo"/>
where id = #{id}
</select>
<insert id="insertVetKnowledge" parameterType="VetKnowledge" useGeneratedKeys="true" keyProperty="id">
insert into vet_knowledge
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="title != null and title != ''">title,</if>
<if test="content != null and content != ''">content,</if>
<if test="category != null and category != ''">category,</if>
<if test="sensitiveWords != null">sensitive_words,</if>
<if test="status != null">status,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="title != null and title != ''">#{title},</if>
<if test="content != null and content != ''">#{content},</if>
<if test="category != null and category != ''">#{category},</if>
<if test="sensitiveWords != null">#{sensitiveWords},</if>
<if test="status != null">#{status},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<update id="updateVetKnowledge" parameterType="VetKnowledge">
update vet_knowledge
<trim prefix="SET" suffixOverrides=",">
<if test="title != null and title != ''">title = #{title},</if>
<if test="content != null and content != ''">content = #{content},</if>
<if test="category != null and category != ''">category = #{category},</if>
<if test="sensitiveWords != null">sensitive_words = #{sensitiveWords},</if>
<if test="status != null">status = #{status},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteVetKnowledgeById" parameterType="Long">
delete from vet_knowledge where id = #{id}
</delete>
<delete id="deleteVetKnowledgeByIds" parameterType="String">
delete from vet_knowledge where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

51
chenhai-system/src/main/resources/mapper/vet/VetNotificationMapper.xml

@ -76,12 +76,6 @@
order by create_time desc order by create_time desc
</select> </select>
<select id="countUnreadNotificationsByUserId" parameterType="Long" resultType="Integer">
select count(*) from vet_notification
where user_id = #{userId}
and is_read = 0
<!-- 已删除 and is_deleted = 0 -->
</select>
<insert id="insertVetNotification" parameterType="VetNotification" useGeneratedKeys="true" keyProperty="id"> <insert id="insertVetNotification" parameterType="VetNotification" useGeneratedKeys="true" keyProperty="id">
insert into vet_notification insert into vet_notification
@ -158,32 +152,6 @@
and is_read = 0 and is_read = 0
</update> </update>
<!-- 删除这个cleanExpiredNotifications方法,因为没有is_deleted字段 -->
<!-- <update id="cleanExpiredNotifications" parameterType="Integer">
update vet_notification
set is_deleted = 1
where create_time &lt; date_sub(now(), interval #{days} day)
and is_deleted = 0
</update> -->
<!-- 删除这个deleteVetNotificationById方法,因为没有is_deleted字段 -->
<!-- <update id="deleteVetNotificationById" parameterType="Long">
update vet_notification
set is_deleted = 1
where id = #{id}
</update> -->
<!-- 删除这个deleteVetNotificationByIds方法,因为没有is_deleted字段 -->
<!-- <update id="deleteVetNotificationByIds" parameterType="Long">
update vet_notification
set is_deleted = 1
where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</update> -->
<!-- 如果需要硬删除,可以添加以下方法 -->
<delete id="deleteVetNotificationById" parameterType="Long"> <delete id="deleteVetNotificationById" parameterType="Long">
delete from vet_notification where id = #{id} delete from vet_notification where id = #{id}
</delete> </delete>
@ -194,4 +162,23 @@
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<select id="selectNotificationStatsByUserId" parameterType="Long" resultType="java.util.HashMap">
select
COALESCE(SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END), 0) as unread,
COALESCE(SUM(CASE WHEN is_read = 1 THEN 1 ELSE 0 END), 0) as `read`
from vet_notification
where user_id = #{userId}
</select>
<select id="selectNotificationsByDate" resultMap="VetNotificationResult">
<include refid="selectVetNotificationVo"/>
where user_id = #{userId}
and related_id = #{certificateId}
and type like 'CERT_EXPIRE%'
and DATE(create_time) = DATE(#{date})
order by create_time desc
limit 1
</select>
</mapper> </mapper>

32
chenhai-system/src/main/resources/mapper/vet/VetQualificationMapper.xml

@ -6,10 +6,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="VetQualification" id="VetQualificationResult"> <resultMap type="VetQualification" id="VetQualificationResult">
<result property="qualificationId" column="qualification_id" /> <result property="qualificationId" column="qualification_id" />
<result property="vetId" column="vet_id" />
<result property="userId" column="user_id" />
<result property="realName" column="real_name" /> <result property="realName" column="real_name" />
<result property="idCard" column="id_card" /> <result property="idCard" column="id_card" />
<result property="qualificationType" column="qualification_type" /> <result property="qualificationType" column="qualification_type" />
<result property="qualificationTypeLabel" column="qualification_type_label"/>
<result property="certificateNo" column="certificate_no" /> <result property="certificateNo" column="certificate_no" />
<result property="certificateFiles" column="certificate_files" /> <result property="certificateFiles" column="certificate_files" />
<result property="applyTime" column="apply_time" /> <result property="applyTime" column="apply_time" />
@ -26,14 +27,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="scopeNames" column="scope_names" /> <result property="scopeNames" column="scope_names" />
</resultMap> </resultMap>
<sql id="selectVetQualificationVo">
<!-- <sql id="selectVetQualificationVo">
select qualification_id, vet_id, real_name, id_card, qualification_type, certificate_no, certificate_files, apply_time, audit_time, audit_status, audit_opinion, auditor_id, create_by, create_time, update_by, update_time, remark ,scope_ids ,scope_names from vet_qualification select qualification_id, vet_id, real_name, id_card, qualification_type, certificate_no, certificate_files, apply_time, audit_time, audit_status, audit_opinion, auditor_id, create_by, create_time, update_by, update_time, remark ,scope_ids ,scope_names from vet_qualification
</sql>-->
<sql id="selectVetQualificationVo">
select
vq.*,
<!-- 资质类型:从字典表获取标签 -->
sd1.dict_label as qualification_type_label,
<!-- 经营范围:从字典表获取 -->
<!-- 注意:这里需要处理多个scope_ids的情况 -->
vq.scope_names as scope_names
from vet_qualification vq
left join sys_dict_data sd1 on sd1.dict_value = vq.qualification_type
and sd1.dict_type = 'qualification_type'
</sql> </sql>
<select id="selectVetQualificationList" parameterType="VetQualification" resultMap="VetQualificationResult"> <select id="selectVetQualificationList" parameterType="VetQualification" resultMap="VetQualificationResult">
<include refid="selectVetQualificationVo"/> <include refid="selectVetQualificationVo"/>
<where> <where>
<if test="vetId != null "> and vet_id = #{vetId}</if>
<if test="userId != null "> and user_id = #{userId}</if>
<if test="realName != null and realName != ''"> and real_name like concat('%', #{realName}, '%')</if> <if test="realName != null and realName != ''"> and real_name like concat('%', #{realName}, '%')</if>
<if test="idCard != null and idCard != ''"> and id_card = #{idCard}</if> <if test="idCard != null and idCard != ''"> and id_card = #{idCard}</if>
<if test="qualificationType != null and qualificationType != ''"> and qualification_type = #{qualificationType}</if> <if test="qualificationType != null and qualificationType != ''"> and qualification_type = #{qualificationType}</if>
@ -56,7 +70,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertVetQualification" parameterType="VetQualification" useGeneratedKeys="true" keyProperty="qualificationId"> <insert id="insertVetQualification" parameterType="VetQualification" useGeneratedKeys="true" keyProperty="qualificationId">
insert into vet_qualification insert into vet_qualification
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="vetId != null">vet_id,</if>
<if test="userId != null">user_id,</if>
<if test="realName != null and realName != ''">real_name,</if> <if test="realName != null and realName != ''">real_name,</if>
<if test="idCard != null and idCard != ''">id_card,</if> <if test="idCard != null and idCard != ''">id_card,</if>
<if test="qualificationType != null and qualificationType != ''">qualification_type,</if> <if test="qualificationType != null and qualificationType != ''">qualification_type,</if>
@ -76,7 +90,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="scopeNames != null and scopeNames != ''">scope_names,</if> <if test="scopeNames != null and scopeNames != ''">scope_names,</if>
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="vetId != null">#{vetId},</if>
<if test="userId != null">#{userId},</if>
<if test="realName != null and realName != ''">#{realName},</if> <if test="realName != null and realName != ''">#{realName},</if>
<if test="idCard != null and idCard != ''">#{idCard},</if> <if test="idCard != null and idCard != ''">#{idCard},</if>
<if test="qualificationType != null and qualificationType != ''">#{qualificationType},</if> <if test="qualificationType != null and qualificationType != ''">#{qualificationType},</if>
@ -100,7 +114,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<update id="updateVetQualification" parameterType="VetQualification"> <update id="updateVetQualification" parameterType="VetQualification">
update vet_qualification update vet_qualification
<trim prefix="SET" suffixOverrides=","> <trim prefix="SET" suffixOverrides=",">
<if test="vetId != null">vet_id = #{vetId},</if>
<if test="userId != null">user_id = #{userId},</if>
<if test="realName != null and realName != ''">real_name = #{realName},</if> <if test="realName != null and realName != ''">real_name = #{realName},</if>
<if test="idCard != null and idCard != ''">id_card = #{idCard},</if> <if test="idCard != null and idCard != ''">id_card = #{idCard},</if>
<if test="qualificationType != null and qualificationType != ''">qualification_type = #{qualificationType},</if> <if test="qualificationType != null and qualificationType != ''">qualification_type = #{qualificationType},</if>
@ -133,10 +147,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach> </foreach>
</delete> </delete>
<update id="submitAudit" parameterType="Long">
update vet_qualification
set audit_status = '0',
apply_time = now()
where qualification_id = #{qualificationId}
</update>
</mapper> </mapper>

110
chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml

@ -0,0 +1,110 @@
<?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.chenhai.vet.mapper.VetTrainingVideoMapper">
<resultMap id="VideoResult" type="VetTrainingVideo">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="title" column="title"/>
<result property="description" column="description"/>
<result property="videoUrl" column="video_url"/>
<result property="coverImage" column="cover_image"/>
<result property="category" column="category"/>
<result property="tags" column="tags"/>
<result property="duration" column="duration"/>
<result property="fileSize" column="file_size"/>
<result property="viewCount" column="view_count"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="userName" column="user_name"/>
</resultMap>
<insert id="insertVideo" parameterType="VetTrainingVideo" useGeneratedKeys="true" keyProperty="id">
INSERT INTO vet_training_video (
user_id, title, description, video_url, cover_image,
category, tags, duration, file_size, view_count,
status, create_time, update_time
) VALUES (
#{vetId}, #{title}, #{description}, #{videoUrl}, #{coverImage},
#{category}, #{tags}, #{duration}, #{fileSize}, #{viewCount},
#{status}, #{createTime}, #{updateTime}
)
</insert>
<select id="selectMyVideos" resultMap="VideoResult">
SELECT v.*, u.nick_name as user_name
FROM vet_training_video v
LEFT JOIN sys_user u ON v.user_id = u.user_id
WHERE v.user_id = #{userId}
<if test="title != null and title != ''">
AND v.title LIKE CONCAT('%', #{title}, '%')
</if>
<if test="category != null and category != ''">
AND v.category = #{category}
</if>
<if test="status != null and status != ''">
AND v.status = #{status}
</if>
ORDER BY v.create_time DESC
</select>
<select id="selectPublicVideos" resultMap="VideoResult">
SELECT v.*, u.nick_name as user_name
FROM vet_training_video v
LEFT JOIN sys_user u ON v.user_id = u.user_id
WHERE v.status = '1'
<if test="title != null and title != ''">
AND v.title LIKE CONCAT('%', #{title}, '%')
</if>
<if test="category != null and category != ''">
AND v.category = #{category}
</if>
<if test="userName != null and userName != ''">
AND u.nick_name LIKE CONCAT('%', #{vetName}, '%')
</if>
ORDER BY v.create_time DESC
</select>
<select id="selectVideoById" resultMap="VideoResult">
SELECT v.*, u.nick_name as user_name
FROM vet_training_video v
LEFT JOIN sys_user u ON v.user_id = u.user_id
WHERE v.id = #{id}
</select>
<update id="incrementViewCount">
UPDATE vet_training_video
SET view_count = view_count + 1
WHERE id = #{id}
</update>
<select id="selectHotVideos" resultMap="VideoResult">
SELECT v.*, u.nick_name as user_name
FROM vet_training_video v
LEFT JOIN sys_user u ON v.user_id = u.user_id
WHERE v.status = '1'
ORDER BY v.view_count DESC
LIMIT #{limit}
</select>
<select id="searchVideos" resultMap="VideoResult">
SELECT v.*, u.nick_name as user_name
FROM vet_training_video v
LEFT JOIN sys_user u ON v.user_id = u.user_id
WHERE v.status = '1'
AND (v.title LIKE CONCAT('%', #{keyword}, '%')
OR v.description LIKE CONCAT('%', #{keyword}, '%')
OR v.tags LIKE CONCAT('%', #{keyword}, '%')
OR u.nick_name LIKE CONCAT('%', #{keyword}, '%'))
ORDER BY v.create_time DESC
</select>
<update id="deleteVideoById">
UPDATE vet_training_video
SET del_flag = '1'
WHERE id = #{id}
</update>
</mapper>

4
chenhai-ui/package.json

@ -29,7 +29,7 @@
"clipboard": "2.0.8", "clipboard": "2.0.8",
"core-js": "3.37.1", "core-js": "3.37.1",
"echarts": "5.4.0", "echarts": "5.4.0",
"element-ui": "2.15.14",
"element-ui": "^2.15.14",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"fuse.js": "6.4.3", "fuse.js": "6.4.3",
"highlight.js": "9.18.5", "highlight.js": "9.18.5",
@ -41,7 +41,7 @@
"screenfull": "5.0.2", "screenfull": "5.0.2",
"sortablejs": "1.10.2", "sortablejs": "1.10.2",
"splitpanes": "2.4.1", "splitpanes": "2.4.1",
"vue": "2.6.12",
"vue": "^2.6.12",
"vue-count-to": "1.0.13", "vue-count-to": "1.0.13",
"vue-cropper": "0.5.5", "vue-cropper": "0.5.5",
"vue-router": "3.4.9", "vue-router": "3.4.9",

60
chenhai-ui/src/api/vet/knowledge.js

@ -0,0 +1,60 @@
import request from '@/utils/request'
// 查询兽医文章列表
export function listKnowledge(query) {
return request({
url: '/vet/knowledge/list',
method: 'get',
params: query
})
}
// 查询兽医文章详细
export function getKnowledge(id) {
return request({
url: '/vet/knowledge/' + id,
method: 'get'
})
}
// 新增兽医文章
export function addKnowledge(data) {
return request({
url: '/vet/knowledge',
method: 'post',
data: data
})
}
// 修改兽医文章
export function updateKnowledge(data) {
return request({
url: '/vet/knowledge',
method: 'put',
data: data
})
}
// 删除兽医文章
export function delKnowledge(id) {
return request({
url: '/vet/knowledge/' + id,
method: 'delete'
})
}
// 上传文章(待审核)
export function uploadKnowledge(data) {
return request({
url: '/vet/knowledge/upload',
method: 'post',
data: data
})
}
// 发布文章
export function publishKnowledge(id) {
return request({
url: '/vet/knowledge/publish/' + id,
method: 'put'
})
}

95
chenhai-ui/src/api/vet/training.js

@ -0,0 +1,95 @@
// src/api/vet/training.js
import request from '@/utils/request'
// 兽医培训视频相关接口
export default {
// 上传视频
uploadVideo(data) {
const formData = new FormData()
Object.keys(data).forEach(key => {
if (data[key] !== undefined && data[key] !== null) {
if (key === 'videoFile' || key === 'coverImage') {
formData.append(key, data[key])
} else {
formData.append(key, data[key])
}
}
})
return request({
url: '/vet/training/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 300000 // 5分钟超时,视频上传需要时间
})
},
// 获取公开视频列表
getPublicVideos(params) {
return request({
url: '/vet/training/public-videos',
method: 'get',
params
})
},
// 获取我的视频
getMyVideos(params = {}) {
return request({
url: '/vet/training/my-videos',
method: 'get',
params: {
pageNum: params.pageNum || 1,
pageSize: params.pageSize || 10,
title: params.title || '',
category: params.category || '',
status: params.status || ''
}
})
},
// 获取视频详情
getVideoDetail(id) {
return request({
url: `/vet/training/video/${id}`,
method: 'get'
})
},
// 获取播放地址(可选)
getPlayUrl(id) {
return request({
url: `/vet/training/video/play/${id}`,
method: 'get'
})
},
// 删除视频
deleteVideo(id) {
return request({
url: `/vet/training/${id}`,
method: 'delete'
})
},
// 搜索视频
searchVideos(keyword) {
return request({
url: '/vet/training/search',
method: 'get',
params: { keyword }
})
},
// 获取热门视频
getHotVideos(limit = 10) {
return request({
url: '/vet/training/hot-videos',
method: 'get',
params: { limit }
})
}
}

10
chenhai-ui/src/views/vet/certificate/index.vue

@ -299,11 +299,11 @@ export default {
/** 查询兽医执业证书列表 */ /** 查询兽医执业证书列表 */
getList() { getList() {
this.loading = false this.loading = false
// listCertificate(this.queryParams).then(response => {
// this.certificateList = response.rows
// this.total = response.total
// this.loading = false
// })
listCertificate(this.queryParams).then(response => {
this.certificateList = response.rows
this.total = response.total
this.loading = false
})
}, },
// //
cancel() { cancel() {

356
chenhai-ui/src/views/vet/knowledge/index.vue

@ -0,0 +1,356 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="文章标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入文章标题"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="文章分类" prop="category">
<!-- 优化分类改为下拉选择避免手动输入不规范 -->
<el-select
v-model="queryParams.category"
placeholder="请选择文章分类"
clearable
@keyup.enter.native="handleQuery"
>
<el-option label="治疗防治" value="治疗防治" />
<el-option label="饲养管理" value="饲养管理" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
@keyup.enter.native="handleQuery"
>
<el-option label="待审核" value="0" />
<el-option label="已发布" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-upload2"
size="mini"
@click="handleUpload"
v-hasPermi="['vet:knowledge:upload']"
>上传文章</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-check"
size="mini"
:disabled="single"
@click="handlePublish"
v-hasPermi="['vet:knowledge:publish']"
>发布文章</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['vet:knowledge:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['vet:knowledge:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['vet:knowledge:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="knowledgeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键" align="center" prop="id" />
<el-table-column label="文章标题" align="center" prop="title" min-width="200" />
<el-table-column label="文章分类" align="center" prop="category" width="120" />
<!-- 优化状态显示为标签样式更直观 -->
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === '0'" type="warning">待审核</el-tag>
<el-tag v-if="scope.row.status === '1'" type="success">已发布</el-tag>
</template>
</el-table-column>
<el-table-column label="创建人" align="center" prop="createBy" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['vet:knowledge:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-check"
@click="handlePublish(scope.row.id)"
v-hasPermi="['vet:knowledge:publish']"
v-if="scope.row.status === '0'"
>发布</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['vet:knowledge:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加/上传/修改兽医文章对话框 -->
<el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="文章标题" prop="title">
<el-input v-model="form.title" placeholder="请输入文章标题" />
</el-form-item>
<el-form-item label="文章内容" prop="content">
<editor v-model="form.content" :min-height="200"/>
</el-form-item>
<el-form-item label="文章分类" prop="category">
<!-- 优化分类下拉选择固定选项 -->
<el-select v-model="form.category" placeholder="请选择文章分类">
<el-option label="治疗防治" value="治疗防治" />
<el-option label="饲养管理" value="饲养管理" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
// APIuploadKnowledgepublishKnowledge
import { listKnowledge, getKnowledge, delKnowledge, addKnowledge, updateKnowledge, uploadKnowledge, publishKnowledge } from "@/api/vet/knowledge"
export default {
name: "Knowledge",
data() {
return {
//
loading: true,
//
ids: [],
//
single: true,
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
knowledgeList: [],
//
title: "",
//
open: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
title: null,
category: null,
status: null, //
},
//
form: {},
//
rules: {
title: [
{ required: true, message: "文章标题不能为空", trigger: "blur" }
],
content: [
{ required: true, message: "文章内容不能为空", trigger: "blur" }
],
category: [
{ required: true, message: "文章分类不能为空", trigger: "change" }
],
}
}
},
created() {
this.getList()
},
methods: {
/** 查询兽医文章列表 */
getList() {
this.loading = true
listKnowledge(this.queryParams).then(response => {
this.knowledgeList = response.rows
this.total = response.total
this.loading = false
})
},
//
cancel() {
this.open = false
this.reset()
},
//
reset() {
this.form = {
id: null,
title: null,
content: null,
category: null,
status: null, //
createBy: null,
createTime: null,
updateBy: null,
updateTime: null,
remark: null
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.queryParams = {
pageNum: 1,
pageSize: 10,
title: null,
category: null,
status: null,
}
this.handleQuery()
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 上传文章按钮操作(替代原新增) */
handleUpload() {
this.reset()
this.open = true
this.title = "上传兽医文章"
},
/** 发布文章按钮操作 */
handlePublish(id) {
// /
const ids = id || this.ids
if (ids.length === 0) {
this.$modal.msgWarning("请选择需要发布的文章")
return
}
this.$modal.confirm('是否确认发布选中的文章?').then(() => {
return publishKnowledge(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess("发布成功")
}).catch(() => {})
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id = row.id || this.ids
getKnowledge(id).then(response => {
this.form = response.data
this.open = true
this.title = "修改兽医文章"
})
},
/** 提交按钮(区分上传/修改) */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
//
updateKnowledge(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
//
uploadKnowledge(this.form).then(response => {
this.$modal.msgSuccess(response.msg || "上传成功")
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids
this.$modal.confirm('是否确认删除兽医文章编号为"' + ids + '"的数据项?').then(() => {
return delKnowledge(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('vet/knowledge/export', {
...this.queryParams
}, `knowledge_${new Date().getTime()}.xlsx`)
}
}
}
</script>

960
chenhai-ui/src/views/vet/training/TrainingHome.vue

@ -0,0 +1,960 @@
<!-- src/views/vet/training/TrainingHome.vue -->
<template>
<div class="training-home">
<!-- 顶部导航 -->
<div class="training-header">
<h2><i class="el-icon-video-camera"></i> 兽医培训中心</h2>
<div class="header-actions">
<el-button
type="primary"
icon="el-icon-upload"
@click="activeTab = 'upload'"
>
上传视频
</el-button>
<el-button
plain
@click="activeTab = 'list'"
>
返回列表
</el-button>
</div>
</div>
<!-- 主内容区 -->
<div class="training-main">
<!-- 左侧菜单 -->
<div class="sidebar">
<el-menu
:default-active="activeTab"
class="training-menu"
@select="handleMenuSelect"
>
<el-menu-item index="list">
<i class="el-icon-video-camera"></i>
<span slot="title">视频列表</span>
</el-menu-item>
<el-menu-item index="upload">
<i class="el-icon-upload"></i>
<span slot="title">上传视频</span>
</el-menu-item>
<el-menu-item index="my">
<i class="el-icon-user"></i>
<span slot="title">我的视频</span>
</el-menu-item>
</el-menu>
</div>
<!-- 右侧内容区 -->
<div class="content-area">
<!-- ================= 视频列表页 ================= -->
<div v-if="activeTab === 'list'" class="tab-content">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索视频标题"
clearable
@keyup.enter.native="loadPublicVideos"
@clear="loadPublicVideos"
style="width: 300px"
>
<i slot="prefix" class="el-icon-search"></i>
</el-input>
<el-select
v-model="filterCategory"
placeholder="全部分类"
clearable
@change="loadPublicVideos"
style="width: 120px"
>
<el-option label="手术技巧" value="surgery" />
<el-option label="疾病诊断" value="diagnosis" />
<el-option label="药物使用" value="medication" />
<el-option label="其他" value="other" />
</el-select>
<el-button
type="primary"
icon="el-icon-refresh"
@click="loadPublicVideos"
>
刷新
</el-button>
</div>
<!-- 视频列表 -->
<h3 style="margin: 20px 0 10px 0; color: #333;">
培训视频
<span v-if="videos.length > 0" style="font-size: 14px; color: #999; margin-left: 10px;">
( {{ videos.length }} )
</span>
</h3>
<div v-if="loading" class="loading">
<el-skeleton :rows="6" animated />
</div>
<div v-else-if="videos.length === 0" class="empty">
<el-empty description="暂无视频"></el-empty>
</div>
<div v-else class="video-grid">
<div
v-for="video in videos"
:key="video.id"
class="video-card"
@click="showVideoDetail(video)"
>
<div class="cover">
<img v-if="video.coverImage" :src="video.coverImage" :alt="video.title" />
<div v-else class="no-cover">
<i class="el-icon-video-camera"></i>
</div>
<div class="duration">{{ formatDuration(video.duration) }}</div>
</div>
<div class="info">
<h4 class="title">{{ video.title }}</h4>
<div class="meta">
<span class="author">
<i class="el-icon-user"></i>
{{ video.nickName || video.userName || '未知' }}
</span>
<span class="views">
<i class="el-icon-view"></i>
{{ video.viewCount || 0 }}次观看
</span>
</div>
<div class="time">{{ formatTime(video.createTime) }}</div>
</div>
</div>
</div>
</div>
<!-- ================= 上传视频页 ================= -->
<div v-else-if="activeTab === 'upload'" class="tab-content">
<h3 style="margin-bottom: 20px; color: #333;">上传培训视频</h3>
<div class="upload-area" @click="handleUploadAreaClick">
<input
ref="fileInput"
type="file"
accept="video/*"
style="display: none"
@change="handleFileSelect"
/>
<div v-if="!selectedFile" class="upload-placeholder">
<i class="el-icon-upload upload-icon"></i>
<p>点击选择视频文件</p>
<p class="hint">支持 mp4avimov 等格式最大 1GB</p>
</div>
<div v-else class="file-info">
<i class="el-icon-video-camera file-icon"></i>
<div class="file-details">
<div class="file-name">{{ selectedFile.name }}</div>
<div class="file-size">{{ formatFileSize(selectedFile.size) }}</div>
</div>
<el-button type="text" @click.stop="removeFile">
<i class="el-icon-close"></i>
</el-button>
</div>
</div>
<!-- 视频信息表单 -->
<div class="upload-form">
<el-form :model="uploadForm" label-width="80px" style="margin-top: 20px;">
<el-form-item label="视频标题" required>
<el-input
v-model="uploadForm.title"
placeholder="请输入视频标题"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="分类">
<el-select v-model="uploadForm.category" placeholder="请选择分类" style="width: 100%">
<el-option label="手术技巧" value="surgery" />
<el-option label="疾病诊断" value="diagnosis" />
<el-option label="药物使用" value="medication" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="uploadForm.description"
type="textarea"
:rows="3"
placeholder="请输入视频描述"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="视频状态">
<el-radio-group v-model="uploadForm.status">
<el-radio label="1">公开所有人可见</el-radio>
<el-radio label="0">私有仅自己可见</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="uploading"
:disabled="!selectedFile || !uploadForm.title"
@click="handleUpload"
>
{{ uploading ? '上传中...' : '开始上传' }}
</el-button>
<el-button @click="resetUploadForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- ================= 我的视频页 ================= -->
<div v-else-if="activeTab === 'my'" class="tab-content">
<div class="my-header">
<h3 style="margin: 0; color: #333;">我的视频</h3>
<el-input
v-model="mySearchKeyword"
placeholder="搜索我的视频"
clearable
@keyup.enter.native="loadMyVideos"
@clear="loadMyVideos"
style="width: 200px"
>
<i slot="prefix" class="el-icon-search"></i>
</el-input>
</div>
<div v-if="myVideosLoading" class="loading">
<el-skeleton :rows="3" animated />
</div>
<div v-else-if="myVideos.length === 0" class="empty">
<el-empty description="你还没有上传过视频">
<el-button type="primary" @click="activeTab = 'upload'">
去上传
</el-button>
</el-empty>
</div>
<div v-else class="my-video-list">
<div class="my-video-item" v-for="video in myVideos" :key="video.id">
<div class="video-info" @click="showVideoDetail(video)">
<div class="left">
<img v-if="video.coverImage" :src="video.coverImage" class="cover-small" />
<div v-else class="cover-small-placeholder">
<i class="el-icon-video-camera"></i>
</div>
<div class="details">
<div class="title">{{ video.title }}</div>
<div class="meta">
<el-tag :type="video.status === '1' ? 'success' : 'warning'" size="small">
{{ video.status === '1' ? '公开' : '私有' }}
</el-tag>
<span class="time">{{ formatTime(video.createTime) }}</span>
<span class="views">{{ video.viewCount }}次观看</span>
</div>
</div>
</div>
<div class="right">
<el-button
size="small"
type="danger"
@click.stop="deleteMyVideo(video.id)"
>
删除
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ================= 视频详情弹窗 ================= -->
<el-dialog
:visible.sync="showDetailDialog"
:title="currentVideo ? currentVideo.title : '视频详情'"
width="800px"
@close="handleDialogClose"
>
<div v-if="currentVideo" class="video-detail-dialog">
<!-- 视频播放器 -->
<div class="video-player">
<video
v-if="currentVideo.videoUrl"
:src="currentVideo.videoUrl"
controls
autoplay
style="width: 100%; max-height: 400px;"
></video>
<div v-else class="no-video">
<i class="el-icon-video-camera"></i>
<p>视频加载失败</p>
</div>
</div>
<!-- 视频信息 -->
<div class="video-info-card">
<div class="info-row">
<span class="label">发布者</span>
<span class="value">{{ currentVideo.nickName || currentVideo.userName || '未知' }}</span>
</div>
<div class="info-row">
<span class="label">发布时间</span>
<span class="value">{{ formatTime(currentVideo.createTime) }}</span>
</div>
<div class="info-row">
<span class="label">观看次数</span>
<span class="value">{{ currentVideo.viewCount || 0 }}</span>
</div>
<div v-if="currentVideo.description" class="info-row">
<span class="label">视频描述</span>
<p class="value">{{ currentVideo.description }}</p>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import trainingApi from '@/api/vet/training'
export default {
name: 'TrainingHome',
data() {
return {
//
activeTab: 'list',
// ========== ==========
searchKeyword: '',
filterCategory: '',
videos: [],
loading: false,
// ========== ==========
fileInput: null,
selectedFile: null,
uploading: false,
uploadForm: {
title: '',
category: '',
description: '',
status: '1'
},
// ========== ==========
mySearchKeyword: '',
myVideos: [],
myVideosLoading: false,
// ========== ==========
showDetailDialog: false,
currentVideo: null
}
},
mounted() {
this.loadPublicVideos()
},
methods: {
//
handleMenuSelect(index) {
this.activeTab = index
if (index === 'list') {
this.loadPublicVideos()
} else if (index === 'my') {
this.loadMyVideos()
}
},
//
handleUploadAreaClick() {
if (this.$refs.fileInput) {
this.$refs.fileInput.click()
}
},
//
async loadPublicVideos() {
try {
this.loading = true
const params = {
title: this.searchKeyword,
category: this.filterCategory
}
const res = await trainingApi.getPublicVideos(params)
this.videos = res.data ? res.data.rows : []
} catch (error) {
console.error('加载视频失败:', error)
this.$message.error('加载失败')
} finally {
this.loading = false
}
},
//
async loadMyVideos() {
try {
this.myVideosLoading = true
const params = {
title: this.mySearchKeyword
}
const res = await trainingApi.getMyVideos(params)
this.myVideos = res.data ? res.data.rows : []
} catch (error) {
console.error('加载我的视频失败:', error)
this.$message.error('加载失败')
} finally {
this.myVideosLoading = false
}
},
//
handleFileSelect(event) {
const input = event.target
if (!input) return
const file = input.files[0]
if (!file) return
// 1GB
if (file.size > 1024 * 1024 * 1024) {
this.$message.error('文件大小不能超过 1GB')
return
}
//
if (!file.type.startsWith('video/')) {
this.$message.error('请选择视频文件')
return
}
this.selectedFile = file
},
//
removeFile() {
this.selectedFile = null
if (this.$refs.fileInput) {
this.$refs.fileInput.value = ''
}
},
//
async handleUpload() {
if (!this.selectedFile) {
this.$message.error('请选择视频文件')
return
}
if (!this.uploadForm.title.trim()) {
this.$message.error('请输入视频标题')
return
}
try {
this.uploading = true
const formData = new FormData()
formData.append('title', this.uploadForm.title)
formData.append('videoFile', this.selectedFile)
formData.append('status', this.uploadForm.status)
if (this.uploadForm.category) {
formData.append('category', this.uploadForm.category)
}
if (this.uploadForm.description) {
formData.append('description', this.uploadForm.description)
}
await trainingApi.uploadVideo(formData)
this.$message.success('上传成功!')
this.resetUploadForm()
this.activeTab = 'my'
this.loadMyVideos()
} catch (error) {
console.error('上传失败:', error)
this.$message.error(error.message || '上传失败,请重试')
} finally {
this.uploading = false
}
},
//
resetUploadForm() {
this.selectedFile = null
this.uploadForm.title = ''
this.uploadForm.category = ''
this.uploadForm.description = ''
this.uploadForm.status = '1'
if (this.$refs.fileInput) {
this.$refs.fileInput.value = ''
}
},
//
showVideoDetail(video) {
this.currentVideo = video
this.showDetailDialog = true
},
//
async deleteMyVideo(videoId) {
try {
await this.$confirm('确定要删除这个视频吗?删除后无法恢复。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await trainingApi.deleteVideo(videoId)
this.$message.success('删除成功')
this.loadMyVideos()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
this.$message.error('删除失败')
}
}
},
//
handleDialogClose() {
this.currentVideo = null
},
// ========== ==========
formatDuration(seconds) {
if (!seconds) return '00:00'
const min = Math.floor(seconds / 60)
const sec = seconds % 60
return `${min}:${sec.toString().padStart(2, '0')}`
},
formatTime(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN')
},
formatFileSize(bytes) {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
}
}
</script>
<style scoped>
.training-home {
height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
.training-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: white;
border-bottom: 1px solid #e8e8e8;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.training-header h2 {
margin: 0;
color: #1890ff;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.training-main {
flex: 1;
display: flex;
background: #f0f2f5;
overflow: hidden;
}
.sidebar {
width: 180px;
background: white;
border-right: 1px solid #e8e8e8;
}
.training-menu {
border-right: none;
}
.training-menu .el-menu-item {
height: 50px;
line-height: 50px;
font-size: 14px;
}
.content-area {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.tab-content {
background: white;
border-radius: 8px;
padding: 20px;
min-height: 500px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
/* 搜索栏样式 */
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
align-items: center;
}
.my-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
/* 视频列表样式 */
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
}
.video-card {
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
background: white;
}
.video-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.video-card .cover {
position: relative;
height: 140px;
background: #f5f5f5;
overflow: hidden;
}
.video-card .cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-cover {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #d9d9d9;
}
.no-cover .el-icon {
font-size: 40px;
}
.duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.video-card .info {
padding: 12px;
}
.video-card .title {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.video-card .meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #8c8c8c;
margin-bottom: 4px;
}
.video-card .meta span {
display: flex;
align-items: center;
gap: 4px;
}
.video-card .time {
font-size: 12px;
color: #bfbfbf;
}
/* 上传区域样式 */
.upload-area {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s;
background: #fafafa;
margin-bottom: 20px;
}
.upload-area:hover {
border-color: #409eff;
}
.upload-placeholder {
color: #909399;
}
.upload-icon {
font-size: 48px;
margin-bottom: 16px;
color: #409eff;
}
.hint {
font-size: 12px;
color: #c0c4cc;
margin-top: 8px;
}
.file-info {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.file-icon {
font-size: 36px;
color: #409eff;
}
.file-details {
flex: 1;
text-align: left;
}
.file-name {
font-weight: 500;
margin-bottom: 4px;
word-break: break-all;
}
.file-size {
font-size: 12px;
color: #909399;
}
.upload-form {
max-width: 600px;
}
/* 我的视频列表样式 */
.my-video-list {
margin-top: 20px;
}
.my-video-item {
border: 1px solid #e8e8e8;
border-radius: 8px;
margin-bottom: 12px;
background: white;
transition: border-color 0.3s;
}
.my-video-item:hover {
border-color: #1890ff;
}
.video-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
cursor: pointer;
}
.video-info .left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.cover-small {
width: 80px;
height: 45px;
border-radius: 4px;
object-fit: cover;
}
.cover-small-placeholder {
width: 80px;
height: 45px;
background: #f5f5f5;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #d9d9d9;
}
.details {
flex: 1;
}
.details .title {
margin: 0 0 6px 0;
font-weight: 500;
color: #333;
}
.details .meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: #8c8c8c;
}
/* 视频详情弹窗样式 */
.video-detail-dialog {
padding: 10px 0;
}
.video-player {
margin-bottom: 20px;
background: #000;
border-radius: 6px;
overflow: hidden;
}
.no-video {
padding: 60px;
text-align: center;
color: white;
background: #666;
}
.no-video .el-icon {
font-size: 48px;
margin-bottom: 16px;
}
.video-info-card {
padding: 16px;
background: #fafafa;
border-radius: 6px;
}
.info-row {
margin-bottom: 10px;
font-size: 14px;
}
.info-row .label {
color: #666;
font-weight: 500;
margin-right: 8px;
}
.info-row .value {
color: #333;
}
/* 加载和空状态 */
.loading, .empty {
padding: 60px 0;
text-align: center;
color: #8c8c8c;
}
/* 响应式设计 */
@media (max-width: 768px) {
.training-main {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid #e8e8e8;
}
.video-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.search-bar {
flex-direction: column;
align-items: stretch;
}
.search-bar .el-input {
width: 100% !important;
}
.my-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
}
</style>
Loading…
Cancel
Save