From 13fc2a6fd62bcc353a6ddb75d2bec6349bba3403 Mon Sep 17 00:00:00 2001 From: ChaiNingQi <2032830459@qq.com> Date: Thu, 15 Jan 2026 09:43:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=AA=E4=BA=BA=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=AE=A1=E7=90=86=E7=9A=84=E8=AF=81=E4=B9=A6=E8=AF=A6?= =?UTF-8?q?=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vet/VetCertificateController.java | 146 ++++- .../vet/VetExperienceArticleController.java | 87 ++- .../vet/VetTrainingVideoController.java | 19 +- .../src/main/resources/application.yml | 4 +- .../vet/domain/VetExperienceArticle.java | 14 +- .../vet/mapper/VetArticleCategoryMapper.java | 36 +- .../mapper/VetExperienceArticleMapper.java | 5 + .../service/IVetExperienceArticleService.java | 2 + .../impl/VetExperienceArticleServiceImpl.java | 14 +- .../mapper/vet/VetArticleCategoryMapper.xml | 34 ++ .../mapper/vet/VetExperienceArticleMapper.xml | 22 +- .../mapper/vet/VetTrainingVideoMapper.xml | 11 +- chenhai-ui/src/api/vet/training.js | 17 +- .../src/views/vet/training/TrainingHome.vue | 578 ++++++++++++++++-- 14 files changed, 826 insertions(+), 163 deletions(-) create mode 100644 chenhai-system/src/main/resources/mapper/vet/VetArticleCategoryMapper.xml diff --git a/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java b/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java index 4cf303a..3ac43af 100644 --- a/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java +++ b/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java @@ -11,7 +11,9 @@ import com.chenhai.vet.domain.VetCertificate; import com.chenhai.vet.service.IVetCertificateService; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -19,7 +21,7 @@ import java.util.Map; /** * 兽医执业证书Controller - * + * * @author ruoyi * @date 2025-12-29 */ @@ -30,6 +32,35 @@ public class VetCertificateController extends BaseController @Autowired private IVetCertificateService vetCertificateService; + /** + * 初始化数据绑定,处理空字符串转换问题 + * 防止前端传递空字符串导致Long类型绑定失败 + */ + @InitBinder + public void initBinder(WebDataBinder binder) { + // 处理Long类型字段的空字符串问题 + binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, true) { + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (text == null || text.trim().isEmpty()) { + setValue(null); // 将空字符串转换为null + } else { + try { + setValue(Long.parseLong(text.trim())); + } catch (NumberFormatException e) { + setValue(null); // 转换失败也设为null + } + } + } + + @Override + public String getAsText() { + Object value = getValue(); + return (value != null ? value.toString() : ""); + } + }); + } + /** * 获取当前用户ID */ @@ -61,9 +92,10 @@ public class VetCertificateController extends BaseController return certificate != null && currentUserId.equals(certificate.getUserId()); } - /** - * 查询兽医执业证书列表 + * 查询兽医执业证书列表(证书管理页面使用) + * 管理员:查看所有证书 + * 普通用户:查看自己的证书 */ @PreAuthorize("@ss.hasPermi('vet:certificate:list')") @GetMapping("/list") @@ -71,21 +103,52 @@ public class VetCertificateController extends BaseController { startPage(); Long currentUserId = getCurrentUserId(); + if (currentUserId == null) { return getDataTable(List.of()); } - // 如果是管理员,可以查看所有证书(不过滤用户ID) + // 管理员可以查看所有,普通用户只能查看自己的 if (!isAdmin()) { - // 普通用户:只查询自己的证书 vetCertificate.setUserId(currentUserId); } - // 管理员:不设置userId,查询所有证书 + // 管理员不设置userId,查看所有 List list = vetCertificateService.selectVetCertificateList(vetCertificate); return getDataTable(list); } + /** + * 用户详情页查询证书(专用于用户详情页) + * 查看指定用户的证书列表 + */ + @PreAuthorize("@ss.hasPermi('vet:certificate:list')") + @GetMapping("/listForDetail") + public TableDataInfo listForDetail(@RequestParam(value = "userId", required = false) Long userId) + { + startPage(); + Long currentUserId = getCurrentUserId(); + + if (currentUserId == null) { + return getDataTable(List.of()); + } + + // 如果没有传userId,返回空 + if (userId == null) { + return getDataTable(List.of()); + } + + // 权限检查:管理员或查看自己 + if (!isAdmin() && !userId.equals(currentUserId)) { + return getDataTable(List.of()); + } + + VetCertificate query = new VetCertificate(); + query.setUserId(userId); + List list = vetCertificateService.selectVetCertificateList(query); + return getDataTable(list); + } + /** * 导出兽医执业证书列表 */ @@ -106,6 +169,10 @@ public class VetCertificateController extends BaseController @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") Long id) { + // 权限检查 + if (!canAccessCertificate(id)) { + return error("没有权限查看此证书"); + } return success(vetCertificateService.selectVetCertificateById(id)); } @@ -123,6 +190,13 @@ public class VetCertificateController extends BaseController return error("用户未登录"); } + // 权限检查:普通用户只能给自己添加证书 + if (!isAdmin() && vetCertificate.getUserId() != null && + !vetCertificate.getUserId().equals(currentUserId)) { + return error("没有权限为其他用户添加证书"); + } + + // 设置用户ID和创建人 vetCertificate.setUserId(currentUserId); vetCertificate.setCreateBy(SecurityUtils.getUsername()); @@ -137,6 +211,20 @@ public class VetCertificateController extends BaseController @PutMapping public AjaxResult edit(@RequestBody VetCertificate vetCertificate) { + // 权限检查 + if (!canAccessCertificate(vetCertificate.getId())) { + return error("没有权限修改此证书"); + } + + // 如果是普通用户,确保不能修改用户ID + if (!isAdmin()) { + Long currentUserId = getCurrentUserId(); + if (currentUserId != null && vetCertificate.getUserId() != null && + !vetCertificate.getUserId().equals(currentUserId)) { + return error("不能修改证书所属用户"); + } + } + return toAjax(vetCertificateService.updateVetCertificate(vetCertificate)); } @@ -145,19 +233,34 @@ public class VetCertificateController extends BaseController */ @PreAuthorize("@ss.hasPermi('vet:certificate:remove')") @Log(title = "兽医执业证书", businessType = BusinessType.DELETE) - @DeleteMapping("/{ids}") + @DeleteMapping("/{ids}") public AjaxResult remove(@PathVariable Long[] ids) { + // 权限检查:检查是否所有证书都可以删除 + if (!isAdmin()) { + for (Long id : ids) { + if (!canAccessCertificate(id)) { + return error("没有权限删除证书ID: " + id); + } + } + } + return toAjax(vetCertificateService.deleteVetCertificateByIds(ids)); } - /** - * 根据用户ID获取证书列表 + * 根据用户ID获取证书列表(兼容旧版本,建议前端迁移到listForDetail) */ @GetMapping("/user/{userId}") public AjaxResult getByUserId(@PathVariable Long userId) { + Long currentUserId = getCurrentUserId(); + + // 权限检查 + if (!isAdmin() && !userId.equals(currentUserId)) { + return error("没有权限查看其他用户的证书"); + } + List list = vetCertificateService.selectCertificatesByUserId(userId); return success(list); } @@ -168,6 +271,13 @@ public class VetCertificateController extends BaseController @GetMapping("/expiring/{userId}") public AjaxResult getExpiringCertificates(@PathVariable Long userId) { + Long currentUserId = getCurrentUserId(); + + // 权限检查 + if (!isAdmin() && !userId.equals(currentUserId)) { + return error("没有权限查看其他用户的证书"); + } + List list = vetCertificateService.selectExpiringCertificates(userId); return success(list); } @@ -178,6 +288,13 @@ public class VetCertificateController extends BaseController @GetMapping("/statistics/{userId}") public AjaxResult getStatistics(@PathVariable Long userId) { + Long currentUserId = getCurrentUserId(); + + // 权限检查 + if (!isAdmin() && !userId.equals(currentUserId)) { + return error("没有权限查看其他用户的统计信息"); + } + Map statistics = vetCertificateService.getCertificateStatistics(userId); return success(statistics); } @@ -188,7 +305,14 @@ public class VetCertificateController extends BaseController @PostMapping("/manual-check/{userId}") public AjaxResult manualCheck(@PathVariable Long userId) { + Long currentUserId = getCurrentUserId(); + + // 权限检查 + if (!isAdmin() && !userId.equals(currentUserId)) { + return error("没有权限为其他用户检查证书"); + } + vetCertificateService.manualCheckCertificates(userId); - return success("检查完成"); + return success("证书检查完成"); } -} +} \ No newline at end of file diff --git a/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java b/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java index 3d5c083..e7fd494 100644 --- a/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java +++ b/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java @@ -7,7 +7,9 @@ import com.chenhai.common.core.page.TableDataInfo; import com.chenhai.common.enums.BusinessType; import com.chenhai.common.utils.SecurityUtils; import com.chenhai.common.utils.poi.ExcelUtil; +import com.chenhai.vet.domain.VetArticleCategory; import com.chenhai.vet.domain.VetExperienceArticle; +import com.chenhai.vet.mapper.VetArticleCategoryMapper; import com.chenhai.vet.service.IVetExperienceArticleService; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +28,9 @@ public class VetExperienceArticleController extends BaseController { @Autowired private IVetExperienceArticleService vetExperienceArticleService; + @Autowired // 确保有这个注解 + private VetArticleCategoryMapper vetArticleCategoryMapper; + /** * 查询兽医经验文章列表 @@ -65,6 +70,53 @@ public class VetExperienceArticleController extends BaseController { return toAjax(vetExperienceArticleService.deleteVetExperienceArticleByIds(ids)); } + @GetMapping("/options") + public AjaxResult getCategoryOptions() { + VetArticleCategory query = new VetArticleCategory(); + query.setStatus("0"); + + List categories = vetArticleCategoryMapper.selectVetArticleCategoryList(query); + + // 排序(如果sortOrder为null,放到最后) + if (categories != null) { + categories.sort((c1, c2) -> { + Integer order1 = c1.getSortOrder() != null ? c1.getSortOrder() : 999; + Integer order2 = c2.getSortOrder() != null ? c2.getSortOrder() : 999; + return order1.compareTo(order2); + }); + } + + List> options = new ArrayList<>(); + for (VetArticleCategory category : categories) { + Map option = new LinkedHashMap<>(); + option.put("value", category.getId()); + option.put("label", category.getName()); + options.add(option); + } + + return AjaxResult.success(options); + } + + /** + * 根据分类查询文章 + */ + @GetMapping("/{categoryId}/articles") + public TableDataInfo getArticlesByCategory( + @PathVariable Long categoryId, + @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum, + @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) { + + startPage(); + + // 创建查询对象 + VetExperienceArticle query = new VetExperienceArticle(); + query.setStatus("1"); // 只查询已发布的文章 + query.setCategoryId(categoryId); // 设置分类ID + + List list = vetExperienceArticleService.selectVetExperienceArticleList(query); + return getDataTable(list); + } + /** @@ -111,13 +163,13 @@ public class VetExperienceArticleController extends BaseController { // 3. 判断是否是当前用户发布的文章 Long currentUserId = getUserId(); - boolean isOwner = currentUserId != null && currentUserId.equals(article.getVetId()); + boolean isOwner = currentUserId != null && currentUserId.equals(article.getUserId()); // 4. 获取相关文章 List relatedArticles = vetExperienceArticleService.selectRelatedArticles(id, article.getCategoryId(), 4); // 5. 获取作者的其他文章 - List authorArticles = vetExperienceArticleService.selectArticlesByVetId(article.getVetId(), 5); + List authorArticles = vetExperienceArticleService.selectArticlesByVetId(article.getUserId(), 5); Map result = new HashMap<>(); result.put("article", article); @@ -131,7 +183,7 @@ public class VetExperienceArticleController extends BaseController { /** * 发布新文章(论坛发布接口) */ - @PreAuthorize("@ss.hasPermi('vet:article:add')") + /*@PreAuthorize("@ss.hasPermi('vet:article:add')")*/ @Log(title = "发布经验文章", businessType = BusinessType.INSERT) @PostMapping("/forum/publish") public AjaxResult publishForumArticle(@RequestBody VetExperienceArticle article) { @@ -139,7 +191,7 @@ public class VetExperienceArticleController extends BaseController { String currentUsername = SecurityUtils.getUsername(); // 设置作者信息 - article.setVetId(currentUserId); + article.setUserId(currentUserId); article.setVetName(currentUsername); // 暂时用用户名,可以根据需要从用户表获取真实姓名 // 设置文章状态为已发布 @@ -174,7 +226,7 @@ public class VetExperienceArticleController extends BaseController { /** * 获取当前用户的文章 */ - @PreAuthorize("@ss.hasPermi('vet:article:my')") + /* @PreAuthorize("@ss.hasPermi('vet:article:my')")*/ @GetMapping("/forum/myArticles") public TableDataInfo getMyForumArticles( @RequestParam(value = "status", required = false) String status) { @@ -182,7 +234,7 @@ public class VetExperienceArticleController extends BaseController { Long currentUserId = getUserId(); VetExperienceArticle query = new VetExperienceArticle(); - query.setVetId(currentUserId); + query.setUserId(currentUserId); if (status != null && !status.isEmpty()) { query.setStatus(status); @@ -247,10 +299,29 @@ public class VetExperienceArticleController extends BaseController { return success(hotTags); } + /** + * 2. 根据标签搜索文章(带分页) + */ + @GetMapping("/listByTag") + public TableDataInfo listByTag(@RequestParam("tag") String tag, + @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum, + @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) { + startPage(); + + // 创建查询对象 + VetExperienceArticle query = new VetExperienceArticle(); + query.setStatus("1"); // 只查已发布的 + query.setTags(tag); // 设置标签条件 + + List list = vetExperienceArticleService.selectVetExperienceArticleList(query); + return getDataTable(list); + } + + /** * 新增文章 - 调整为论坛发布模式 */ - @PreAuthorize("@ss.hasPermi('vet:article:add')") + /* @PreAuthorize("@ss.hasPermi('vet:article:add')")*/ @Log(title = "兽医经验文章", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody VetExperienceArticle vetExperienceArticle) { @@ -282,7 +353,7 @@ public class VetExperienceArticleController extends BaseController { /** * 导出兽医经验文章列表 */ - @PreAuthorize("@ss.hasPermi('vet:article:export')") + /* @PreAuthorize("@ss.hasPermi('vet:article:export')")*/ @Log(title = "兽医经验文章", businessType = BusinessType.EXPORT) @PostMapping("/export") public void export(HttpServletResponse response, VetExperienceArticle vetExperienceArticle) { diff --git a/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java b/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java index 7ac6859..8621498 100644 --- a/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java +++ b/chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java @@ -31,29 +31,26 @@ public class VetTrainingVideoController extends BaseController { */ @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) { + @RequestParam("title") String title, // 改为 @RequestParam + @RequestParam("videoFile") MultipartFile videoFile, // 文件字段保持 @RequestPart 或改为 @RequestParam + @RequestParam(value = "description", required = false) String description, + @RequestParam(value = "category", required = false) String category, + @RequestParam(value = "status", required = false, defaultValue = "1") String status) { Long userId = getCurrentUserId(); try { VetTrainingVideo video = new VetTrainingVideo(); - video.setUserId(userId); // 修正变量名 + 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); + String result = trainingVideoService.uploadAndSave(video, videoFile, null); return success(result); } catch (Exception e) { + e.printStackTrace(); return error("上传失败:" + e.getMessage()); } } diff --git a/chenhai-admin/src/main/resources/application.yml b/chenhai-admin/src/main/resources/application.yml index 47044b7..acb3d99 100644 --- a/chenhai-admin/src/main/resources/application.yml +++ b/chenhai-admin/src/main/resources/application.yml @@ -115,8 +115,8 @@ token: # 微信小程序配置 wx: muhu: - app-id: wxb5becc8d6d8123a6 - app-secret: 74f4211d3985aa782ff9148aa00f824e + app-id: wx9bf7f4e81a1b2d6b + app-secret: b743202d5c1029ab214d94026524c4b7 vet: app-id: ${WX_MINI_APPID:your_app_id} app-secret: ${WX_MINI_SECRET:your_app_secret} diff --git a/chenhai-system/src/main/java/com/chenhai/vet/domain/VetExperienceArticle.java b/chenhai-system/src/main/java/com/chenhai/vet/domain/VetExperienceArticle.java index 3ece9d5..294e828 100644 --- a/chenhai-system/src/main/java/com/chenhai/vet/domain/VetExperienceArticle.java +++ b/chenhai-system/src/main/java/com/chenhai/vet/domain/VetExperienceArticle.java @@ -42,7 +42,7 @@ public class VetExperienceArticle extends BaseEntity /** 作者(兽医)ID */ @Excel(name = "作者", readConverterExp = "兽=医") - private Long vetId; + private Long userId; /** 兽医姓名 */ @Excel(name = "兽医姓名") @@ -61,7 +61,7 @@ public class VetExperienceArticle extends BaseEntity private Long categoryId; /** 分类名称 */ - @Excel(name = "分类名称") + @Excel(name = "分类名称",dictType = "vet_experience_category") private String categoryName; /** 标签(逗号分隔) */ @@ -190,14 +190,14 @@ public class VetExperienceArticle extends BaseEntity return images; } - 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 setVetName(String vetName) @@ -329,7 +329,7 @@ public class VetExperienceArticle extends BaseEntity .append("summary", getSummary()) .append("coverImage", getCoverImage()) .append("images", getImages()) - .append("vetId", getVetId()) + .append("userId", getUserId()) .append("vetName", getVetName()) .append("vetAvatar", getVetAvatar()) .append("vetTitle", getVetTitle()) diff --git a/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetArticleCategoryMapper.java b/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetArticleCategoryMapper.java index 35abbb8..89bc71a 100644 --- a/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetArticleCategoryMapper.java +++ b/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetArticleCategoryMapper.java @@ -4,44 +4,10 @@ import com.chenhai.vet.domain.VetArticleCategory; import org.apache.ibatis.annotations.Mapper; import java.util.List; -/** - * 文章分类Mapper接口 - */ @Mapper public interface VetArticleCategoryMapper { - /** - * 查询文章分类 - */ - VetArticleCategory selectVetArticleCategoryById(Long id); - - /** - * 查询文章分类列表 + * 这个方法名必须和XML中的id完全一致 */ List selectVetArticleCategoryList(VetArticleCategory vetArticleCategory); - - /** - * 查询启用的分类列表 - */ - List selectEnabledCategories(); - - /** - * 新增文章分类 - */ - int insertVetArticleCategory(VetArticleCategory vetArticleCategory); - - /** - * 修改文章分类 - */ - int updateVetArticleCategory(VetArticleCategory vetArticleCategory); - - /** - * 删除文章分类 - */ - int deleteVetArticleCategoryById(Long id); - - /** - * 批量删除文章分类 - */ - int deleteVetArticleCategoryByIds(Long[] ids); } \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetExperienceArticleMapper.java b/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetExperienceArticleMapper.java index 1492df4..96be52e 100644 --- a/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetExperienceArticleMapper.java +++ b/chenhai-system/src/main/java/com/chenhai/vet/mapper/VetExperienceArticleMapper.java @@ -86,4 +86,9 @@ public interface VetExperienceArticleMapper * 增加收藏数 */ int incrementCollectCount(@Param("id") Long id); + + /** + * 根据标签筛选文章 + */ + List selectArticlesByTag(@Param("tag") String tag); } diff --git a/chenhai-system/src/main/java/com/chenhai/vet/service/IVetExperienceArticleService.java b/chenhai-system/src/main/java/com/chenhai/vet/service/IVetExperienceArticleService.java index e8f2987..97dbb2c 100644 --- a/chenhai-system/src/main/java/com/chenhai/vet/service/IVetExperienceArticleService.java +++ b/chenhai-system/src/main/java/com/chenhai/vet/service/IVetExperienceArticleService.java @@ -112,4 +112,6 @@ public interface IVetExperienceArticleService * 获取热门标签 */ List> selectHotTags(Integer limit); + + List selectArticlesByTag(String tag); } diff --git a/chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetExperienceArticleServiceImpl.java b/chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetExperienceArticleServiceImpl.java index ceec20d..f0ebdde 100644 --- a/chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetExperienceArticleServiceImpl.java +++ b/chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetExperienceArticleServiceImpl.java @@ -193,7 +193,7 @@ public class VetExperienceArticleServiceImpl implements IVetExperienceArticleSer // 获取兽医总数(去重) Set vetIds = new HashSet<>(); for (VetExperienceArticle article : allArticles) { - vetIds.add(article.getVetId()); + vetIds.add(article.getUserId()); } statistics.put("totalVets", vetIds.size()); @@ -214,6 +214,17 @@ public class VetExperienceArticleServiceImpl implements IVetExperienceArticleSer return statistics; } + /** + * 根据标签筛选文章(简单实现) + */ + @Override + public List selectArticlesByTag(String tag) { + if (tag == null || tag.trim().isEmpty()) { + return new ArrayList<>(); + } + return vetExperienceArticleMapper.selectArticlesByTag(tag.trim()); + } + @Override public List> selectHotTags(Integer limit) { // 从所有文章中提取标签并统计热度 @@ -248,4 +259,5 @@ public class VetExperienceArticleServiceImpl implements IVetExperienceArticleSer return hotTags; } + } diff --git a/chenhai-system/src/main/resources/mapper/vet/VetArticleCategoryMapper.xml b/chenhai-system/src/main/resources/mapper/vet/VetArticleCategoryMapper.xml new file mode 100644 index 0000000..817a1c8 --- /dev/null +++ b/chenhai-system/src/main/resources/mapper/vet/VetArticleCategoryMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + select id, name, description, icon, sort_order, status, create_time, update_time + from vet_article_category + + + + + + \ No newline at end of file diff --git a/chenhai-system/src/main/resources/mapper/vet/VetExperienceArticleMapper.xml b/chenhai-system/src/main/resources/mapper/vet/VetExperienceArticleMapper.xml index c9c1478..ac467c1 100644 --- a/chenhai-system/src/main/resources/mapper/vet/VetExperienceArticleMapper.xml +++ b/chenhai-system/src/main/resources/mapper/vet/VetExperienceArticleMapper.xml @@ -11,7 +11,7 @@ - + @@ -32,7 +32,7 @@ - select id, title, content, summary, cover_image, images, vet_id, vet_name, vet_avatar, vet_title, category_id, category_name, tags, view_count, like_count, collect_count, is_top, is_featured, status, is_sensitive, sensitive_words, publish_time, create_time, update_time + select id, title, content, summary, cover_image, images, user_id, vet_name, vet_avatar, vet_title, category_id, category_name, tags, view_count, like_count, collect_count, is_top, is_featured, status, is_sensitive, sensitive_words, publish_time, create_time, update_time from vet_experience_article @@ -46,7 +46,7 @@ and title like concat('%', #{title}, '%') - and vet_id = #{vetId} + and user_id = #{userId} and vet_name like concat('%', #{vetName}, '%') and category_id = #{categoryId} and category_name like concat('%', #{categoryName}, '%') @@ -73,7 +73,7 @@ or tags like concat('%', #{keyword}, '%') ) - and vet_id = #{vetId} + and user_id = #{userId} and category_id = #{categoryId} and is_featured = #{isFeatured} and is_top = #{isTop} @@ -150,7 +150,7 @@ summary, cover_image, images, - vet_id, + user_id, vet_name, vet_avatar, vet_title, @@ -175,7 +175,7 @@ #{summary}, #{coverImage}, #{images}, - #{vetId}, + #{userId}, #{vetName}, #{vetAvatar}, #{vetTitle}, @@ -204,7 +204,7 @@ summary = #{summary}, cover_image = #{coverImage}, images = #{images}, - vet_id = #{vetId}, + user_id = #{userId}, vet_name = #{vetName}, vet_avatar = #{vetAvatar}, vet_title = #{vetTitle}, @@ -235,4 +235,12 @@ #{id} + + \ No newline at end of file diff --git a/chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml b/chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml index 68b5fca..6bc522a 100644 --- a/chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml +++ b/chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml @@ -28,7 +28,7 @@ category, tags, duration, file_size, view_count, status, create_time, update_time ) VALUES ( - #{vetId}, #{title}, #{description}, #{videoUrl}, #{coverImage}, + #{userId}, #{title}, #{description}, #{videoUrl}, #{coverImage}, #{category}, #{tags}, #{duration}, #{fileSize}, #{viewCount}, #{status}, #{createTime}, #{updateTime} ) @@ -63,7 +63,7 @@ AND v.category = #{category} - AND u.nick_name LIKE CONCAT('%', #{vetName}, '%') + AND u.nick_name LIKE CONCAT('%', #{userName}, '%') ORDER BY v.create_time DESC @@ -102,9 +102,8 @@ ORDER BY v.create_time DESC - - UPDATE vet_training_video - SET del_flag = '1' + + DELETE FROM vet_training_video WHERE id = #{id} - + \ No newline at end of file diff --git a/chenhai-ui/src/api/vet/training.js b/chenhai-ui/src/api/vet/training.js index 10d1ee3..320adec 100644 --- a/chenhai-ui/src/api/vet/training.js +++ b/chenhai-ui/src/api/vet/training.js @@ -5,26 +5,17 @@ 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]) - } - } - }) - + // 注意:这里直接返回请求,不要重新创建 FormData return request({ url: '/vet/training/upload', method: 'post', - data: formData, + data: data, // data 已经是 FormData 对象 headers: { 'Content-Type': 'multipart/form-data' }, - timeout: 300000 // 5分钟超时,视频上传需要时间 + timeout: 300000 }) + }, // 获取公开视频列表 diff --git a/chenhai-ui/src/views/vet/training/TrainingHome.vue b/chenhai-ui/src/views/vet/training/TrainingHome.vue index 2dac1d3..d253247 100644 --- a/chenhai-ui/src/views/vet/training/TrainingHome.vue +++ b/chenhai-ui/src/views/vet/training/TrainingHome.vue @@ -110,22 +110,41 @@ @click="showVideoDetail(video)" >
- -
- + + + + +
+
+ + {{ getCategoryText(video.category) }} +
+
{{ formatDuration(video.duration) }}
+
-

{{ video.title }}

+
{{ video.title }}
- + - {{ video.nickName || video.userName || '未知' }} + {{ video.userName || '未知' }} - + - {{ video.viewCount || 0 }}次观看 + {{ video.viewCount || 0 }}
{{ formatTime(video.createTime) }}
@@ -251,9 +270,14 @@
- -
- +
+ +
+ +
{{ video.title }}
@@ -262,7 +286,7 @@ {{ video.status === '1' ? '公开' : '私有' }} {{ formatTime(video.createTime) }} - {{ video.viewCount }}次观看 + {{ video.viewCount || 0 }}次观看
@@ -287,29 +311,66 @@ :visible.sync="showDetailDialog" :title="currentVideo ? currentVideo.title : '视频详情'" width="800px" + @open="handleDialogOpen" @close="handleDialogClose" >
- -
+
+ +

加载视频中...

+
+ +

视频加载失败

+

可能的原因:

+
    +
  • 视频文件不存在
  • +
  • 网络连接问题
  • +
  • 视频格式不支持
  • +
+
+ + 重试加载 + + + 在新标签页打开 + + + 关闭 + +
+ +
发布者: - {{ currentVideo.nickName || currentVideo.userName || '未知' }} + {{ currentVideo.userName || '未知' }} +
+
+ 分类: + {{ getCategoryText(currentVideo.category) }}
发布时间: @@ -321,7 +382,15 @@
视频描述: -

{{ currentVideo.description }}

+

{{ currentVideo.description }}

+
+
+ 视频地址: +

+ + {{ currentVideo.videoUrl }} + +

@@ -363,7 +432,14 @@ export default { // ========== 视频详情相关 ========== showDetailDialog: false, - currentVideo: null + currentVideo: null, + videoPlayerReady: false, + videoLoadFailed: false, + + // ========== 封面相关 ========== + coverLoadingStates: new Map(), + coverErrorStates: new Map(), + defaultCoverCache: new Map(), } }, @@ -393,14 +469,28 @@ export default { async loadPublicVideos() { try { this.loading = true + console.log('🔄 加载公开视频...') + const params = { title: this.searchKeyword, - category: this.filterCategory + category: this.filterCategory, + pageNum: 1, + pageSize: 100 } + const res = await trainingApi.getPublicVideos(params) - this.videos = res.data ? res.data.rows : [] + console.log('✅ 公开视频响应:', res) + + if (res && res.rows) { + console.log(`📊 找到 ${res.rows.length} 个视频`) + this.videos = res.rows + } else { + console.warn('⚠️ 公开视频响应数据格式不正确') + this.videos = [] + } + } catch (error) { - console.error('加载视频失败:', error) + console.error('❌ 加载公开视频失败:', error) this.$message.error('加载失败') } finally { this.loading = false @@ -411,13 +501,27 @@ export default { async loadMyVideos() { try { this.myVideosLoading = true + console.log('🔄 加载我的视频...') + const params = { - title: this.mySearchKeyword + title: this.mySearchKeyword, + pageNum: 1, + pageSize: 100 } + const res = await trainingApi.getMyVideos(params) - this.myVideos = res.data ? res.data.rows : [] + console.log('✅ 我的视频响应:', res) + + if (res && res.rows) { + console.log(`📊 找到 ${res.rows.length} 个我的视频`) + this.myVideos = res.rows + } else { + console.warn('⚠️ 我的视频响应数据格式不正确') + this.myVideos = [] + } + } catch (error) { - console.error('加载我的视频失败:', error) + console.error('❌ 加载我的视频失败:', error) this.$message.error('加载失败') } finally { this.myVideosLoading = false @@ -511,10 +615,141 @@ export default { } }, + // ========== 视频详情相关方法 ========== + // 显示视频详情 showVideoDetail(video) { - this.currentVideo = video - this.showDetailDialog = true + console.log('🔍 查看视频详情,原始数据:', video) + + if (!video || !video.videoUrl) { + this.$message.error('视频信息不完整') + return + } + + // 获取完整URL + const fullVideoUrl = this.getVideoUrl(video.videoUrl) + console.log('📹 视频URL分析:') + console.log(' 原始URL:', video.videoUrl) + console.log(' 完整URL:', fullVideoUrl) + console.log(' 是否有效URL:', fullVideoUrl.startsWith('http') || fullVideoUrl.startsWith('/')) + + // 测试URL可访问性 + this.testVideoUrl(fullVideoUrl).then(isAccessible => { + if (!isAccessible) { + console.log('⚠️ 视频URL可能无法访问') + } + + this.currentVideo = { + ...video, + videoUrl: fullVideoUrl, + coverImage: this.getCoverImage(video) + } + this.showDetailDialog = true + }).catch(() => { + this.currentVideo = { + ...video, + videoUrl: fullVideoUrl, + coverImage: this.getCoverImage(video) + } + this.showDetailDialog = true + }) + }, + + // 测试视频URL是否可访问 + testVideoUrl(url) { + return new Promise((resolve) => { + const testVideo = document.createElement('video') + testVideo.preload = 'metadata' + testVideo.onloadedmetadata = () => { + console.log('✅ 视频URL可访问,时长:', testVideo.duration) + resolve(true) + } + testVideo.onerror = () => { + console.log('❌ 视频URL不可访问:', url) + resolve(false) + } + + // 设置超时 + setTimeout(() => { + console.log('⏰ 视频加载超时') + resolve(false) + }, 3000) + + testVideo.src = url + }) + }, + + // 弹窗打开时 + handleDialogOpen() { + this.videoPlayerReady = false + this.videoLoadFailed = false + + // 延迟初始化视频播放器 + this.$nextTick(() => { + if (this.$refs.videoPlayer) { + this.$refs.videoPlayer.load() + } + }) + }, + + // 弹窗关闭时 + handleDialogClose() { + // 暂停视频 + if (this.$refs.videoPlayer) { + this.$refs.videoPlayer.pause() + } + this.currentVideo = null + this.videoPlayerReady = false + this.videoLoadFailed = false + }, + + // 视频可以播放时 + handleVideoCanPlay() { + console.log('🎬 视频可以播放了') + this.videoPlayerReady = true + this.videoLoadFailed = false + }, + + // 视频加载完成 + handleVideoLoaded() { + console.log('✅ 视频加载完成') + }, + + // 视频加载错误 + handleVideoError(event) { + console.error('❌ 视频加载错误:', event) + this.videoLoadFailed = true + this.$message.error('视频加载失败,请检查视频文件') + }, + + // 重试加载视频 + retryLoadVideo() { + console.log('🔄 重试加载视频') + this.videoPlayerReady = false + this.videoLoadFailed = false + + if (this.currentVideo && this.currentVideo.videoUrl) { + // 添加时间戳避免缓存 + const videoUrl = this.currentVideo.videoUrl + + (this.currentVideo.videoUrl.includes('?') ? '&' : '?') + + 't=' + Date.now() + + this.currentVideo.videoUrl = videoUrl + + this.$nextTick(() => { + const videoElement = this.$refs.videoPlayer + if (videoElement) { + videoElement.load() + } + }) + } + }, + + // 在新标签页打开视频 + openVideoInNewTab() { + if (this.currentVideo && this.currentVideo.videoUrl) { + window.open(this.currentVideo.videoUrl, '_blank') + } }, // 删除我的视频 @@ -528,19 +763,144 @@ export default { await trainingApi.deleteVideo(videoId) this.$message.success('删除成功') + + // 删除后立即刷新数据 this.loadMyVideos() + // 同时刷新公开视频列表(如果删除的是公开视频) + if (this.activeTab === 'list') { + this.loadPublicVideos() + } + } catch (error) { if (error !== 'cancel') { console.error('删除失败:', error) - this.$message.error('删除失败') + this.$message.error('删除失败: ' + (error.message || '未知错误')) } } }, - // 弹窗关闭处理 - handleDialogClose() { - this.currentVideo = null + // ========== 封面相关方法 ========== + + // 获取封面图片 + getCoverImage(video) { + // 如果视频对象有有效封面,返回封面URL + if (video.coverImage && + video.coverImage !== 'null' && + video.coverImage !== 'undefined' && + video.coverImage !== '') { + return this.getFullUrl(video.coverImage) + } + + // 否则返回默认封面 + return this.getCategoryDefaultCover(video.category || 'other') + }, + + // 根据分类获取默认封面 + getCategoryDefaultCover(category) { + // 缓存默认封面,避免重复生成 + if (this.defaultCoverCache.has(category)) { + return this.defaultCoverCache.get(category) + } + + // 根据分类生成不同的默认封面SVG + const color = this.getCategoryColor(category) + const text = this.getCategoryText(category) + + // 创建SVG + const svgString = ` + + + ${text} + + ` + + // 转换为base64 + const base64 = btoa(unescape(encodeURIComponent(svgString))) + const dataUrl = `data:image/svg+xml;base64,${base64}` + + // 缓存 + this.defaultCoverCache.set(category, dataUrl) + + return dataUrl + }, + + // 获取分类颜色 + getCategoryColor(category) { + const colors = { + surgery: '#667eea', // 蓝色 + diagnosis: '#67cf77', // 绿色 + medication: '#ff6b6b', // 红色 + other: '#ffb366' // 橙色 + } + return colors[category] || colors.other + }, + + // 获取分类文本 + getCategoryText(category) { + const texts = { + surgery: '手术技巧', + diagnosis: '疾病诊断', + medication: '药物使用', + other: '其他' + } + return texts[category] || '其他' + }, + + // 处理封面图片加载成功 + handleCoverImageLoad(videoId) { + console.log(`✅ 封面图片加载成功: ${videoId}`) + this.coverLoadingStates.set(videoId, true) + this.coverErrorStates.set(videoId, false) + }, + + // 处理封面图片加载错误 + handleCoverImageError(video) { + const videoId = video.id + console.log(`❌ 封面图片加载失败: ${videoId}`) + + // 标记为错误状态 + this.coverErrorStates.set(videoId, true) + + // 使用默认封面替换 + this.$nextTick(() => { + const imgElements = document.querySelectorAll(`[data-video-id="${videoId}"] img`) + imgElements.forEach(img => { + img.src = this.getCategoryDefaultCover(video.category || 'other') + }) + }) + }, + + // ========== URL处理方法 ========== + + // 获取完整URL + getFullUrl(url) { + if (!url) return '' + + // 清理可能的空格 + url = url.trim() + + // 如果是完整URL或base64,直接返回 + if (url.startsWith('http://') || + url.startsWith('https://') || + url.startsWith('data:') || + url.startsWith('blob:')) { + return url + } + + // 如果是相对路径(以/开头),直接返回 + if (url.startsWith('/')) { + return window.location.origin + url + } + + // 否则添加uploads前缀 + return window.location.origin + '/uploads/' + url + }, + + // 获取视频URL + getVideoUrl(videoUrl) { + return this.getFullUrl(videoUrl) }, // ========== 工具函数 ========== @@ -548,13 +908,20 @@ export default { if (!seconds) return '00:00' const min = Math.floor(seconds / 60) const sec = seconds % 60 - return `${min}:${sec.toString().padStart(2, '0')}` + return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}` }, formatTime(dateStr) { if (!dateStr) return '' - const date = new Date(dateStr) - return date.toLocaleDateString('zh-CN') + try { + const date = new Date(dateStr) + return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }) + } catch (error) { + return dateStr + } }, formatFileSize(bytes) { @@ -563,7 +930,7 @@ export default { 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] - } + }, } } @@ -594,8 +961,6 @@ export default { gap: 8px; } - - .training-main { flex: 1; display: flex; @@ -680,19 +1045,33 @@ export default { width: 100%; height: 100%; object-fit: cover; + display: block; + transition: opacity 0.3s; } -.no-cover { +/* 默认封面样式 */ +.default-cover { + width: 100%; + height: 100%; display: flex; align-items: center; justify-content: center; - width: 100%; - height: 100%; - color: #d9d9d9; } -.no-cover .el-icon { - font-size: 40px; +.cover-content { + text-align: center; + color: white; +} + +.cover-icon { + font-size: 36px; + margin-bottom: 8px; + display: block; +} + +.cover-text { + font-size: 14px; + font-weight: 500; } .duration { @@ -704,6 +1083,7 @@ export default { padding: 2px 6px; border-radius: 4px; font-size: 12px; + z-index: 2; } .video-card .info { @@ -837,22 +1217,28 @@ export default { flex: 1; } -.cover-small { +.cover-small-container { + position: relative; width: 80px; height: 45px; + flex-shrink: 0; +} + +.cover-small { + width: 100%; + height: 100%; border-radius: 4px; object-fit: cover; } .cover-small-placeholder { - width: 80px; - height: 45px; - background: #f5f5f5; + width: 100%; + height: 100%; border-radius: 4px; display: flex; align-items: center; justify-content: center; - color: #d9d9d9; + color: white; } .details { @@ -863,6 +1249,10 @@ export default { margin: 0 0 6px 0; font-weight: 500; color: #333; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; } .details .meta { @@ -878,25 +1268,79 @@ export default { padding: 10px 0; } -.video-player { - margin-bottom: 20px; - background: #000; +/* 视频加载状态 */ +.video-loading { + padding: 60px; + text-align: center; + color: #666; + background: #f5f5f5; border-radius: 6px; - overflow: hidden; + min-height: 200px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } -.no-video { - padding: 60px; +.video-loading .el-icon-loading { + font-size: 36px; + margin-bottom: 16px; + color: #409eff; +} + +/* 视频错误状态 */ +.video-error { + padding: 40px; text-align: center; - color: white; - background: #666; + color: #f56c6c; + background: #fff5f5; + border-radius: 6px; + min-height: 200px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } -.no-video .el-icon { +.video-error .el-icon-video-camera { font-size: 48px; margin-bottom: 16px; } +.error-tip { + margin: 16px 0 8px 0; + color: #333; + font-weight: 500; +} + +.error-tip ul { + text-align: left; + display: inline-block; + margin: 8px auto; + color: #666; +} + +.error-tip li { + margin: 4px 0; +} + +.error-actions { + margin-top: 20px; + display: flex; + justify-content: center; + gap: 12px; +} + +.video-player { + margin-bottom: 20px; + border-radius: 6px; + overflow: hidden; + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; +} + .video-info-card { padding: 16px; background: #fafafa; @@ -904,18 +1348,28 @@ export default { } .info-row { - margin-bottom: 10px; + margin-bottom: 12px; font-size: 14px; + display: flex; } .info-row .label { color: #666; font-weight: 500; margin-right: 8px; + flex-shrink: 0; + width: 80px; } .info-row .value { color: #333; + flex: 1; +} + +.description-text { + white-space: pre-wrap; + line-height: 1.5; + margin: 0; } /* 加载和空状态 */