“mitao”
2026-03-26 92a2413a3beede5063e05f85f53f99a70fb676d6
feat:系统与安全-系统设置-平台资质
13个文件已添加
5个文件已修改
1459 ■■■■■ 已修改文件
backend/sql/init.sql 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/sql/update_platform_qualification_schema.sql 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/controller/PlatformQualificationController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/dto/PlatformQualificationPublishStatusRequest.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/dto/PlatformQualificationSaveRequest.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/entity/PlatformQualification.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/mapper/PlatformQualificationMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/service/PlatformQualificationAppService.java 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/vo/PlatformQualificationDetailVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/tcm/training/admin/system/vo/PlatformQualificationListItemVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/tcm/training/admin/system/service/PlatformQualificationAppServiceTest.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/config/routes.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/access.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/system/platform-qualification/index.tsx 557 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/system/platform-qualification/service.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/system/platform-qualification/style.less 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/system/settings-redirect/index.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/sql/init.sql
@@ -118,6 +118,12 @@
(1321, 1300, '活动通知', 'system_activity_notice', 'C', '/system/settings/activity-notice', 'System/ActivityNotice', NULL, 13, 1, 1, 'system:activity-notice:view', '活动通知菜单', 0),
(1322, 1321, '新增活动通知', 'system_activity_notice_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:activity-notice:add', '活动通知新增按钮', 0),
(1323, 1321, '删除活动通知', 'system_activity_notice_delete', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:activity-notice:delete', '活动通知删除按钮', 0),
(1324, 1300, '平台资质', 'system_platform_qualification', 'C', '/system/settings/platform-qualification', 'System/PlatformQualification', NULL, 14, 1, 1, 'system:platform-qualification:view', '平台资质菜单', 0),
(1325, 1324, '新增平台资质', 'system_platform_qualification_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:platform-qualification:add', '平台资质新增按钮', 0),
(1326, 1324, '查看平台资质', 'system_platform_qualification_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:platform-qualification:detail', '平台资质详情按钮', 0),
(1327, 1324, '编辑平台资质', 'system_platform_qualification_edit', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:platform-qualification:edit', '平台资质编辑按钮', 0),
(1328, 1324, '删除平台资质', 'system_platform_qualification_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:platform-qualification:delete', '平台资质删除按钮', 0),
(1329, 1324, '切换平台资质上架状态', 'system_platform_qualification_publish_status', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:platform-qualification:publish-status', '平台资质上架状态按钮', 0),
(1310, 1300, '积分设置', 'system_points_settings', 'C', '/system/settings/points', 'System/PointsSettings', NULL, 10, 1, 1, 'system:points-settings:view', '积分设置菜单', 0),
(1311, 1310, '编辑积分设置', 'system_points_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:points-settings:edit', '积分设置编辑按钮', 0),
(2000, 0, '用户与会员', 'member_root', 'M', '/member', NULL, 'team', 200, 1, 1, NULL, '用户与会员模块', 0),
@@ -203,7 +209,7 @@
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
VALUES
(1, 1000), (1, 1100), (1, 1101), (1, 1102), (1, 1103), (1, 1104), (1, 1105),
(1, 1200), (1, 1201), (1, 1202), (1, 1203), (1, 1204), (1, 1300), (1, 1305), (1, 1306), (1, 1307), (1, 1308), (1, 1309), (1, 1310), (1, 1311), (1, 1312), (1, 1313), (1, 1314), (1, 1315), (1, 1316), (1, 1317), (1, 1318), (1, 1319), (1, 1320), (1, 1321), (1, 1322), (1, 1323),
(1, 1200), (1, 1201), (1, 1202), (1, 1203), (1, 1204), (1, 1300), (1, 1305), (1, 1306), (1, 1307), (1, 1308), (1, 1309), (1, 1310), (1, 1311), (1, 1312), (1, 1313), (1, 1314), (1, 1315), (1, 1316), (1, 1317), (1, 1318), (1, 1319), (1, 1320), (1, 1321), (1, 1322), (1, 1323), (1, 1324), (1, 1325), (1, 1326), (1, 1327), (1, 1328), (1, 1329),
(1, 2000), (1, 2100), (1, 2110), (1, 2111), (1, 2112), (1, 2113), (1, 2114), (1, 2115), (1, 2116), (1, 2117), (1, 2118), (1, 2119),
(1, 2200), (1, 2210), (1, 2211), (1, 2220), (1, 2221),
(1, 3000), (1, 3050), (1, 3060), (1, 3061), (1, 3062), (1, 3063),
@@ -916,6 +922,29 @@
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动通知表';
-- ----------------------------
-- 平台资质
-- ----------------------------
CREATE TABLE IF NOT EXISTS `tcm_platform_qualification` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `qualification_name` VARCHAR(64) NOT NULL COMMENT '资质名称',
  `qualification_type` VARCHAR(16) NOT NULL COMMENT '资质类型:RICH_TEXT/LINK',
  `record_number` VARCHAR(64) NOT NULL COMMENT '备案号',
  `content_html` LONGTEXT DEFAULT NULL COMMENT '资质内容富文本',
  `link_url` VARCHAR(512) DEFAULT NULL COMMENT '跳转链接',
  `sort` INT NOT NULL DEFAULT 0 COMMENT '排序',
  `publish_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否上架:1是 0否',
  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_platform_qualification_created_at` (`created_at`),
  KEY `idx_platform_qualification_publish_enabled` (`publish_enabled`),
  KEY `idx_platform_qualification_type` (`qualification_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台资质表';
-- ----------------------------
-- 积分设置
-- ----------------------------
CREATE TABLE IF NOT EXISTS `tcm_points_config` (
backend/sql/update_platform_qualification_schema.sql
New file
@@ -0,0 +1,34 @@
CREATE TABLE IF NOT EXISTS `tcm_platform_qualification` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `qualification_name` VARCHAR(64) NOT NULL COMMENT '资质名称',
  `qualification_type` VARCHAR(16) NOT NULL COMMENT '资质类型:RICH_TEXT/LINK',
  `record_number` VARCHAR(64) NOT NULL COMMENT '备案号',
  `content_html` LONGTEXT DEFAULT NULL COMMENT '资质内容富文本',
  `link_url` VARCHAR(512) DEFAULT NULL COMMENT '跳转链接',
  `sort` INT NOT NULL DEFAULT 0 COMMENT '排序',
  `publish_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否上架:1是 0否',
  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_platform_qualification_created_at` (`created_at`),
  KEY `idx_platform_qualification_publish_enabled` (`publish_enabled`),
  KEY `idx_platform_qualification_type` (`qualification_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台资质表';
INSERT IGNORE INTO `sys_menu`
(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
VALUES
(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
(1324, 1300, '平台资质', 'system_platform_qualification', 'C', '/system/settings/platform-qualification', 'System/PlatformQualification', NULL, 14, 1, 1, 'system:platform-qualification:view', '平台资质菜单', 0),
(1325, 1324, '新增平台资质', 'system_platform_qualification_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:platform-qualification:add', '平台资质新增按钮', 0),
(1326, 1324, '查看平台资质', 'system_platform_qualification_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:platform-qualification:detail', '平台资质详情按钮', 0),
(1327, 1324, '编辑平台资质', 'system_platform_qualification_edit', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:platform-qualification:edit', '平台资质编辑按钮', 0),
(1328, 1324, '删除平台资质', 'system_platform_qualification_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:platform-qualification:delete', '平台资质删除按钮', 0),
(1329, 1324, '切换平台资质上架状态', 'system_platform_qualification_publish_status', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:platform-qualification:publish-status', '平台资质上架状态按钮', 0);
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
VALUES
(1, 1300), (1, 1324), (1, 1325), (1, 1326), (1, 1327), (1, 1328), (1, 1329);
backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
@@ -153,6 +153,18 @@
                null, null, null, 1, "system:activity-notice:add", "活动通知新增按钮"));
        menuIds.add(saveMenu(1323L, 1321L, "删除活动通知", "system_activity_notice_delete", "B",
                null, null, null, 2, "system:activity-notice:delete", "活动通知删除按钮"));
        menuIds.add(saveMenu(1324L, 1300L, "平台资质", "system_platform_qualification", "C",
                "/system/settings/platform-qualification", "System/PlatformQualification", null, 14, "system:platform-qualification:view", "平台资质菜单"));
        menuIds.add(saveMenu(1325L, 1324L, "新增平台资质", "system_platform_qualification_add", "B",
                null, null, null, 1, "system:platform-qualification:add", "平台资质新增按钮"));
        menuIds.add(saveMenu(1326L, 1324L, "查看平台资质", "system_platform_qualification_detail", "B",
                null, null, null, 2, "system:platform-qualification:detail", "平台资质详情按钮"));
        menuIds.add(saveMenu(1327L, 1324L, "编辑平台资质", "system_platform_qualification_edit", "B",
                null, null, null, 3, "system:platform-qualification:edit", "平台资质编辑按钮"));
        menuIds.add(saveMenu(1328L, 1324L, "删除平台资质", "system_platform_qualification_delete", "B",
                null, null, null, 4, "system:platform-qualification:delete", "平台资质删除按钮"));
        menuIds.add(saveMenu(1329L, 1324L, "切换平台资质上架状态", "system_platform_qualification_publish_status", "B",
                null, null, null, 5, "system:platform-qualification:publish-status", "平台资质上架状态按钮"));
        menuIds.add(saveMenu(1310L, 1300L, "积分设置", "system_points_settings", "C",
                "/system/settings/points", "System/PointsSettings", null, 10, "system:points-settings:view", "积分设置菜单"));
        menuIds.add(saveMenu(1311L, 1310L, "编辑积分设置", "system_points_settings_edit", "B",
backend/src/main/java/com/tcm/training/admin/system/controller/PlatformQualificationController.java
New file
@@ -0,0 +1,71 @@
package com.tcm.training.admin.system.controller;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.common.PageResponse;
import com.tcm.training.admin.system.dto.PlatformQualificationPublishStatusRequest;
import com.tcm.training.admin.system.dto.PlatformQualificationSaveRequest;
import com.tcm.training.admin.system.service.PlatformQualificationAppService;
import com.tcm.training.admin.system.vo.PlatformQualificationDetailVO;
import com.tcm.training.admin.system.vo.PlatformQualificationListItemVO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@Validated
@RestController
@RequestMapping({"/api/v1", "/api"})
public class PlatformQualificationController {
    private final PlatformQualificationAppService platformQualificationAppService;
    public PlatformQualificationController(PlatformQualificationAppService platformQualificationAppService) {
        this.platformQualificationAppService = platformQualificationAppService;
    }
    @GetMapping("/platform-qualifications")
    public ApiResponse<PageResponse<PlatformQualificationListItemVO>> page(
            @RequestParam(required = false, defaultValue = "1") int page,
            @RequestParam(required = false, defaultValue = "10") int pageSize,
            @RequestParam(required = false) String qualificationName) {
        return ApiResponse.ok(platformQualificationAppService.page(page, pageSize, qualificationName));
    }
    @GetMapping("/platform-qualifications/{id}")
    public ApiResponse<PlatformQualificationDetailVO> detail(@PathVariable("id") Long id) {
        return ApiResponse.ok(platformQualificationAppService.detail(id));
    }
    @PostMapping("/platform-qualifications")
    public ApiResponse<PlatformQualificationDetailVO> create(@Valid @RequestBody PlatformQualificationSaveRequest request) {
        return ApiResponse.ok(platformQualificationAppService.create(request));
    }
    @PutMapping("/platform-qualifications/{id}")
    public ApiResponse<PlatformQualificationDetailVO> update(@PathVariable("id") Long id,
                                                             @Valid @RequestBody PlatformQualificationSaveRequest request) {
        return ApiResponse.ok(platformQualificationAppService.update(id, request));
    }
    @PatchMapping("/platform-qualifications/{id}/publish-status")
    public ApiResponse<Void> patchPublishStatus(@PathVariable("id") Long id,
                                                @Valid @RequestBody PlatformQualificationPublishStatusRequest request) {
        platformQualificationAppService.patchPublishStatus(id, request);
        return ApiResponse.ok(null);
    }
    @DeleteMapping("/platform-qualifications/{id}")
    public ApiResponse<Void> delete(@PathVariable("id") Long id) {
        platformQualificationAppService.delete(id);
        return ApiResponse.ok(null);
    }
}
backend/src/main/java/com/tcm/training/admin/system/dto/PlatformQualificationPublishStatusRequest.java
New file
@@ -0,0 +1,16 @@
package com.tcm.training.admin.system.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@NoArgsConstructor
public class PlatformQualificationPublishStatusRequest {
    @NotNull(message = "请选择上架状态")
    private Boolean publishEnabled;
}
backend/src/main/java/com/tcm/training/admin/system/dto/PlatformQualificationSaveRequest.java
New file
@@ -0,0 +1,40 @@
package com.tcm.training.admin.system.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Getter
@Setter
@NoArgsConstructor
public class PlatformQualificationSaveRequest {
    @NotBlank(message = "请输入资质名称")
    @Size(max = 64, message = "资质名称长度不能超过64个字符")
    private String qualificationName;
    @NotBlank(message = "请选择资质类型")
    @Size(max = 16, message = "资质类型长度不能超过16个字符")
    private String qualificationType;
    @NotBlank(message = "请输入备案号")
    @Size(max = 64, message = "备案号长度不能超过64个字符")
    private String recordNumber;
    private String contentHtml;
    @Size(max = 512, message = "跳转链接长度不能超过512个字符")
    private String linkUrl;
    @NotNull(message = "请输入排序")
    @Min(value = 0, message = "排序不能小于0")
    private Integer sort;
    @NotNull(message = "请选择上架状态")
    private Boolean publishEnabled;
}
backend/src/main/java/com/tcm/training/admin/system/entity/PlatformQualification.java
New file
@@ -0,0 +1,62 @@
package com.tcm.training.admin.system.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@TableName("tcm_platform_qualification")
@Getter
@Setter
@NoArgsConstructor
public class PlatformQualification {
    public static final String TYPE_RICH_TEXT = "RICH_TEXT";
    public static final String TYPE_LINK = "LINK";
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("qualification_name")
    private String qualificationName;
    @TableField("qualification_type")
    private String qualificationType;
    @TableField("record_number")
    private String recordNumber;
    @TableField("content_html")
    private String contentHtml;
    @TableField("link_url")
    private String linkUrl;
    @TableField("sort")
    private Integer sort;
    @TableField("publish_enabled")
    private Integer publishEnabled;
    @TableLogic
    private Integer deleted;
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;
    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
    @TableField("created_at")
    private LocalDateTime createdAt;
    @TableField("updated_at")
    private LocalDateTime updatedAt;
}
backend/src/main/java/com/tcm/training/admin/system/mapper/PlatformQualificationMapper.java
New file
@@ -0,0 +1,9 @@
package com.tcm.training.admin.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tcm.training.admin.system.entity.PlatformQualification;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PlatformQualificationMapper extends BaseMapper<PlatformQualification> {
}
backend/src/main/java/com/tcm/training/admin/system/service/PlatformQualificationAppService.java
New file
@@ -0,0 +1,197 @@
package com.tcm.training.admin.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tcm.training.admin.common.PageResponse;
import com.tcm.training.admin.system.dto.PlatformQualificationPublishStatusRequest;
import com.tcm.training.admin.system.dto.PlatformQualificationSaveRequest;
import com.tcm.training.admin.system.entity.PlatformQualification;
import com.tcm.training.admin.system.mapper.PlatformQualificationMapper;
import com.tcm.training.admin.system.vo.PlatformQualificationDetailVO;
import com.tcm.training.admin.system.vo.PlatformQualificationListItemVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
public class PlatformQualificationAppService {
    private final PlatformQualificationMapper platformQualificationMapper;
    public PlatformQualificationAppService(PlatformQualificationMapper platformQualificationMapper) {
        this.platformQualificationMapper = platformQualificationMapper;
    }
    public PageResponse<PlatformQualificationListItemVO> page(int page, int pageSize, String qualificationName) {
        LambdaQueryWrapper<PlatformQualification> queryWrapper = new LambdaQueryWrapper<PlatformQualification>()
                .eq(PlatformQualification::getDeleted, 0)
                .like(qualificationName != null && !qualificationName.trim().isEmpty(),
                        PlatformQualification::getQualificationName,
                        qualificationName == null ? null : qualificationName.trim())
                .orderByDesc(PlatformQualification::getCreatedAt)
                .orderByDesc(PlatformQualification::getId);
        IPage<PlatformQualification> result = platformQualificationMapper.selectPage(
                new Page<PlatformQualification>(page, pageSize),
                queryWrapper
        );
        List<PlatformQualificationListItemVO> list = new ArrayList<PlatformQualificationListItemVO>();
        for (PlatformQualification item : result.getRecords()) {
            list.add(toListItem(item));
        }
        return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
    }
    public PlatformQualificationDetailVO detail(Long id) {
        return toDetailVO(requireItem(id));
    }
    @Transactional
    public PlatformQualificationDetailVO create(PlatformQualificationSaveRequest request) {
        PlatformQualification item = new PlatformQualification();
        fillEntity(item, request);
        item.setDeleted(0);
        platformQualificationMapper.insert(item);
        return detail(item.getId());
    }
    @Transactional
    public PlatformQualificationDetailVO update(Long id, PlatformQualificationSaveRequest request) {
        PlatformQualification item = requireItem(id);
        fillEntity(item, request);
        platformQualificationMapper.updateById(item);
        return detail(id);
    }
    @Transactional
    public void delete(Long id) {
        requireItem(id);
        platformQualificationMapper.deleteById(id);
    }
    @Transactional
    public void patchPublishStatus(Long id, PlatformQualificationPublishStatusRequest request) {
        PlatformQualification item = requireItem(id);
        item.setPublishEnabled(booleanToInteger(request.getPublishEnabled()));
        platformQualificationMapper.updateById(item);
    }
    private void fillEntity(PlatformQualification item, PlatformQualificationSaveRequest request) {
        String qualificationType = normalizeQualificationType(request.getQualificationType());
        item.setQualificationName(normalizeRequired(request.getQualificationName(), "请输入资质名称"));
        item.setQualificationType(qualificationType);
        item.setRecordNumber(normalizeRequired(request.getRecordNumber(), "请输入备案号"));
        item.setSort(request.getSort());
        item.setPublishEnabled(booleanToInteger(request.getPublishEnabled()));
        if (PlatformQualification.TYPE_RICH_TEXT.equals(qualificationType)) {
            item.setContentHtml(normalizeContent(request.getContentHtml()));
            item.setLinkUrl(null);
            return;
        }
        item.setLinkUrl(normalizeRequired(request.getLinkUrl(), "请输入跳转链接"));
        item.setContentHtml(null);
    }
    private PlatformQualification requireItem(Long id) {
        PlatformQualification item = platformQualificationMapper.selectOne(new LambdaQueryWrapper<PlatformQualification>()
                .eq(PlatformQualification::getId, id)
                .eq(PlatformQualification::getDeleted, 0)
                .last("limit 1"));
        if (item == null) {
            throw new IllegalArgumentException("平台资质不存在");
        }
        return item;
    }
    private PlatformQualificationListItemVO toListItem(PlatformQualification item) {
        PlatformQualificationListItemVO vo = new PlatformQualificationListItemVO();
        vo.setId(item.getId());
        vo.setQualificationName(item.getQualificationName());
        vo.setQualificationType(item.getQualificationType());
        vo.setRecordNumber(item.getRecordNumber());
        vo.setSort(item.getSort());
        vo.setPublishEnabled(integerToBoolean(item.getPublishEnabled()));
        vo.setCreatedBy(item.getCreatedBy());
        vo.setCreatedAt(item.getCreatedAt());
        return vo;
    }
    private PlatformQualificationDetailVO toDetailVO(PlatformQualification item) {
        PlatformQualificationDetailVO vo = new PlatformQualificationDetailVO();
        vo.setId(item.getId());
        vo.setQualificationName(item.getQualificationName());
        vo.setQualificationType(item.getQualificationType());
        vo.setRecordNumber(item.getRecordNumber());
        vo.setContentHtml(item.getContentHtml());
        vo.setLinkUrl(item.getLinkUrl());
        vo.setSort(item.getSort());
        vo.setPublishEnabled(integerToBoolean(item.getPublishEnabled()));
        vo.setCreatedBy(item.getCreatedBy());
        vo.setUpdatedBy(item.getUpdatedBy());
        vo.setCreatedAt(item.getCreatedAt());
        vo.setUpdatedAt(item.getUpdatedAt());
        return vo;
    }
    private String normalizeQualificationType(String qualificationType) {
        String normalized = normalizeRequired(qualificationType, "请选择资质类型").toUpperCase();
        if (PlatformQualification.TYPE_RICH_TEXT.equals(normalized) || PlatformQualification.TYPE_LINK.equals(normalized)) {
            return normalized;
        }
        throw new IllegalArgumentException("资质类型不合法");
    }
    private String normalizeRequired(String value, String message) {
        String trimmed = trimToNull(value);
        if (trimmed == null) {
            throw new IllegalArgumentException(message);
        }
        return trimmed;
    }
    private String normalizeContent(String contentHtml) {
        String trimmed = trimToNull(contentHtml);
        if (trimmed == null || isBlankRichText(trimmed)) {
            throw new IllegalArgumentException("请输入资质内容");
        }
        return trimmed;
    }
    private boolean isBlankRichText(String html) {
        String normalized = html
                .replaceAll("(?i)<br\\s*/?>", "")
                .replaceAll("(?i)</p>", "")
                .replace("&nbsp;", "")
                .replace("&#160;", "")
                .replaceAll("<[^>]+>", "")
                .trim();
        if (!normalized.isEmpty()) {
            return false;
        }
        String lowerCaseHtml = html.toLowerCase();
        return !lowerCaseHtml.contains("<img")
                && !lowerCaseHtml.contains("<video")
                && !lowerCaseHtml.contains("<audio")
                && !lowerCaseHtml.contains("<iframe");
    }
    private Integer booleanToInteger(Boolean value) {
        return Boolean.TRUE.equals(value) ? 1 : 0;
    }
    private Boolean integerToBoolean(Integer value) {
        return value != null && value == 1;
    }
    private String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
backend/src/main/java/com/tcm/training/admin/system/vo/PlatformQualificationDetailVO.java
New file
@@ -0,0 +1,26 @@
package com.tcm.training.admin.system.vo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
public class PlatformQualificationDetailVO {
    private Long id;
    private String qualificationName;
    private String qualificationType;
    private String recordNumber;
    private String contentHtml;
    private String linkUrl;
    private Integer sort;
    private Boolean publishEnabled;
    private String createdBy;
    private String updatedBy;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
backend/src/main/java/com/tcm/training/admin/system/vo/PlatformQualificationListItemVO.java
New file
@@ -0,0 +1,22 @@
package com.tcm.training.admin.system.vo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
public class PlatformQualificationListItemVO {
    private Long id;
    private String qualificationName;
    private String qualificationType;
    private String recordNumber;
    private Integer sort;
    private Boolean publishEnabled;
    private String createdBy;
    private LocalDateTime createdAt;
}
backend/src/test/java/com/tcm/training/admin/system/service/PlatformQualificationAppServiceTest.java
New file
@@ -0,0 +1,171 @@
package com.tcm.training.admin.system.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.tcm.training.admin.system.dto.PlatformQualificationPublishStatusRequest;
import com.tcm.training.admin.system.dto.PlatformQualificationSaveRequest;
import com.tcm.training.admin.system.entity.PlatformQualification;
import com.tcm.training.admin.system.mapper.PlatformQualificationMapper;
import com.tcm.training.admin.system.vo.PlatformQualificationDetailVO;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PlatformQualificationAppServiceTest {
    @BeforeAll
    static void initLambdaCache() {
        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
        TableInfoHelper.initTableInfo(assistant, PlatformQualification.class);
    }
    @Mock
    private PlatformQualificationMapper platformQualificationMapper;
    @InjectMocks
    private PlatformQualificationAppService platformQualificationAppService;
    @Captor
    private ArgumentCaptor<PlatformQualification> platformQualificationCaptor;
    @Test
    void createShouldPersistRichTextQualification() {
        doAnswer(invocation -> {
            PlatformQualification item = invocation.getArgument(0);
            item.setId(1L);
            return 1;
        }).when(platformQualificationMapper).insert(any(PlatformQualification.class));
        PlatformQualification persisted = new PlatformQualification();
        persisted.setId(1L);
        persisted.setQualificationName("增值电信业务经营许可证");
        persisted.setQualificationType(PlatformQualification.TYPE_RICH_TEXT);
        persisted.setRecordNumber("1654156");
        persisted.setContentHtml("<p>资质内容</p>");
        persisted.setSort(99);
        persisted.setPublishEnabled(1);
        persisted.setCreatedAt(LocalDateTime.of(2026, 3, 26, 21, 10, 0));
        when(platformQualificationMapper.selectOne(any())).thenReturn(persisted);
        PlatformQualificationSaveRequest request = new PlatformQualificationSaveRequest();
        request.setQualificationName("  增值电信业务经营许可证  ");
        request.setQualificationType("rich_text");
        request.setRecordNumber(" 1654156 ");
        request.setContentHtml("<p>资质内容</p>");
        request.setSort(99);
        request.setPublishEnabled(true);
        PlatformQualificationDetailVO result = platformQualificationAppService.create(request);
        verify(platformQualificationMapper).insert(platformQualificationCaptor.capture());
        assertEquals("增值电信业务经营许可证", platformQualificationCaptor.getValue().getQualificationName());
        assertEquals("1654156", platformQualificationCaptor.getValue().getRecordNumber());
        assertEquals("<p>资质内容</p>", platformQualificationCaptor.getValue().getContentHtml());
        assertNull(platformQualificationCaptor.getValue().getLinkUrl());
        assertEquals("增值电信业务经营许可证", result.getQualificationName());
    }
    @Test
    void createShouldPersistLinkQualification() {
        doAnswer(invocation -> {
            PlatformQualification item = invocation.getArgument(0);
            item.setId(2L);
            return 1;
        }).when(platformQualificationMapper).insert(any(PlatformQualification.class));
        PlatformQualification persisted = new PlatformQualification();
        persisted.setId(2L);
        persisted.setQualificationName("营业执照");
        persisted.setQualificationType(PlatformQualification.TYPE_LINK);
        persisted.setRecordNumber("ICP-2026");
        persisted.setLinkUrl("https://example.com/license");
        persisted.setSort(10);
        persisted.setPublishEnabled(0);
        when(platformQualificationMapper.selectOne(any())).thenReturn(persisted);
        PlatformQualificationSaveRequest request = new PlatformQualificationSaveRequest();
        request.setQualificationName("营业执照");
        request.setQualificationType("LINK");
        request.setRecordNumber("ICP-2026");
        request.setLinkUrl(" https://example.com/license ");
        request.setSort(10);
        request.setPublishEnabled(false);
        platformQualificationAppService.create(request);
        verify(platformQualificationMapper).insert(platformQualificationCaptor.capture());
        assertEquals(PlatformQualification.TYPE_LINK, platformQualificationCaptor.getValue().getQualificationType());
        assertEquals("https://example.com/license", platformQualificationCaptor.getValue().getLinkUrl());
        assertNull(platformQualificationCaptor.getValue().getContentHtml());
    }
    @Test
    void createShouldRejectBlankRichTextContent() {
        PlatformQualificationSaveRequest request = new PlatformQualificationSaveRequest();
        request.setQualificationName("活动资质");
        request.setQualificationType("RICH_TEXT");
        request.setRecordNumber("A-001");
        request.setContentHtml("<p><br/></p>");
        request.setSort(1);
        request.setPublishEnabled(true);
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> platformQualificationAppService.create(request));
        assertEquals("请输入资质内容", exception.getMessage());
    }
    @Test
    void createShouldRejectEmptyLinkUrl() {
        PlatformQualificationSaveRequest request = new PlatformQualificationSaveRequest();
        request.setQualificationName("活动资质");
        request.setQualificationType("LINK");
        request.setRecordNumber("A-001");
        request.setLinkUrl("   ");
        request.setSort(1);
        request.setPublishEnabled(true);
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> platformQualificationAppService.create(request));
        assertEquals("请输入跳转链接", exception.getMessage());
    }
    @Test
    void patchPublishStatusShouldUpdateFlag() {
        PlatformQualification persisted = new PlatformQualification();
        persisted.setId(3L);
        persisted.setQualificationName("备案资质");
        persisted.setQualificationType(PlatformQualification.TYPE_LINK);
        persisted.setRecordNumber("A-002");
        persisted.setLinkUrl("https://example.com");
        persisted.setSort(2);
        persisted.setPublishEnabled(0);
        when(platformQualificationMapper.selectOne(any())).thenReturn(persisted);
        PlatformQualificationPublishStatusRequest request = new PlatformQualificationPublishStatusRequest();
        request.setPublishEnabled(true);
        platformQualificationAppService.patchPublishStatus(3L, request);
        verify(platformQualificationMapper).updateById(platformQualificationCaptor.capture());
        assertEquals(Integer.valueOf(1), platformQualificationCaptor.getValue().getPublishEnabled());
    }
}
tcm-training-admin/config/routes.ts
@@ -99,6 +99,12 @@
            component: './system/activity-notice',
          },
          {
            name: '平台资质',
            path: '/system/settings/platform-qualification',
            access: 'system:platform-qualification:view',
            component: './system/platform-qualification',
          },
          {
            name: '消息推送配置',
            path: '/system/settings/message-push',
            access: 'system:message-push:view',
tcm-training-admin/src/access.js
@@ -18,6 +18,7 @@
      !!permission?.['system:app-version:view'] ||
      !!permission?.['system:banner:view'] ||
      !!permission?.['system:activity-notice:view'] ||
      !!permission?.['system:platform-qualification:view'] ||
      !!permission?.['system:message-push:view'] ||
      !!permission?.['system:points-settings:view'],
    canViewMemberModule: canViewMemberUserGroup || canViewMemberVipGroup,
tcm-training-admin/src/pages/system/platform-qualification/index.tsx
New file
@@ -0,0 +1,557 @@
import BraftEditorCom from '@/components/BraftEditor';
import { PlusOutlined } from '@ant-design/icons';
import { history, useAccess } from '@umijs/max';
import { Button, Form, Input, message, Modal, Pagination, Radio, Space, Table, Tag } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import React, { useEffect, useState } from 'react';
import {
  createPlatformQualification,
  deletePlatformQualification,
  getPlatformQualificationDetail,
  getPlatformQualificationPage,
  updatePlatformQualification,
  updatePlatformQualificationPublishStatus,
} from './service';
import './style.less';
type SearchValues = {
  qualificationName?: string;
};
type QualificationItem = {
  id: number;
  qualificationName?: string;
  qualificationType?: string;
  recordNumber?: string;
  sort?: number;
  publishEnabled?: boolean;
  createdBy?: string;
  createdAt?: string;
};
type QualificationDetail = QualificationItem & {
  contentHtml?: string;
  linkUrl?: string;
  updatedAt?: string;
  updatedBy?: string;
};
type QualificationFormValues = {
  qualificationName?: string;
  qualificationType?: string;
  recordNumber?: string;
  contentHtml?: string;
  linkUrl?: string;
  sort?: string;
  publishEnabled?: boolean;
};
type ModalMode = 'create' | 'edit' | 'detail';
const TYPE_RICH_TEXT = 'RICH_TEXT';
const TYPE_LINK = 'LINK';
const typeOptions = [
  { label: '富文本说明', value: TYPE_RICH_TEXT },
  { label: '跳转链接', value: TYPE_LINK },
];
const defaultValues: QualificationFormValues = {
  qualificationName: '',
  qualificationType: TYPE_RICH_TEXT,
  recordNumber: '',
  contentHtml: '',
  linkUrl: '',
  sort: '0',
  publishEnabled: true,
};
const trimSearchValues = (values: SearchValues) => ({
  qualificationName: values.qualificationName?.trim() || undefined,
});
const isBlankRichText = (html?: string) => {
  const value = (html || '').trim();
  if (!value) {
    return true;
  }
  const normalized = value
    .replace(/<br\s*\/?>/gi, '')
    .replace(/<\/p>/gi, '')
    .replace(/&nbsp;/gi, '')
    .replace(/&#160;/gi, '')
    .replace(/<[^>]+>/g, '')
    .trim();
  if (normalized) {
    return false;
  }
  const lowerCaseValue = value.toLowerCase();
  return !lowerCaseValue.includes('<img') && !lowerCaseValue.includes('<video');
};
const validateSort = async (_: unknown, value: string | undefined) => {
  const normalized = String(value || '').trim();
  if (!normalized) {
    throw new Error('请输入排序');
  }
  if (!/^(0|[1-9]\d*)$/.test(normalized)) {
    throw new Error('排序必须为0或正整数');
  }
};
const getTypeLabel = (value?: string) => {
  if (value === TYPE_LINK) {
    return '跳转链接';
  }
  return '富文本说明';
};
const PlatformQualificationPage: React.FC = () => {
  const access = useAccess();
  const [searchForm] = Form.useForm<SearchValues>();
  const [form] = Form.useForm<QualificationFormValues>();
  const [list, setList] = useState<QualificationItem[]>([]);
  const [loading, setLoading] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [modalOpen, setModalOpen] = useState(false);
  const [modalLoading, setModalLoading] = useState(false);
  const [modalMode, setModalMode] = useState<ModalMode>('create');
  const [editingId, setEditingId] = useState<number>();
  const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 });
  const canView = access['system:platform-qualification:view'];
  const canAdd = access['system:platform-qualification:add'];
  const canDetail = access['system:platform-qualification:detail'];
  const canEdit = access['system:platform-qualification:edit'];
  const canDelete = access['system:platform-qualification:delete'];
  const canPublishStatus = access['system:platform-qualification:publish-status'];
  const qualificationType = Form.useWatch('qualificationType', form);
  const isDetailMode = modalMode === 'detail';
  const showRichTextField = qualificationType !== TYPE_LINK;
  const showLinkField = qualificationType === TYPE_LINK;
  const loadList = async (page = pagination.page, pageSize = pagination.pageSize) => {
    setLoading(true);
    try {
      const res = await getPlatformQualificationPage({
        page,
        pageSize,
        ...trimSearchValues(searchForm.getFieldsValue()),
      });
      setList(res.data?.list || []);
      setPagination({
        page: Number(res.data?.page || page),
        pageSize: Number(res.data?.pageSize || pageSize),
        total: Number(res.data?.total || 0),
      });
    } catch (error) {
      message.error((error as Error)?.message || '平台资质列表加载失败');
      setList([]);
    } finally {
      setLoading(false);
    }
  };
  useEffect(() => {
    if (!canView) {
      history.replace('/Welcome');
      return;
    }
    loadList(1, 10);
  }, [canView]);
  const handleCloseModal = () => {
    if (submitting) {
      return;
    }
    setModalOpen(false);
    setModalLoading(false);
    setEditingId(undefined);
    form.resetFields();
  };
  const openCreateModal = () => {
    setModalMode('create');
    setEditingId(undefined);
    setModalLoading(false);
    form.resetFields();
    form.setFieldsValue(defaultValues);
    setModalOpen(true);
  };
  const openModal = async (record: QualificationItem, mode: ModalMode) => {
    setModalMode(mode);
    setEditingId(record.id);
    setModalOpen(true);
    setModalLoading(true);
    form.resetFields();
    try {
      const res = await getPlatformQualificationDetail(record.id);
      const detail = (res.data || {}) as QualificationDetail;
      form.setFieldsValue({
        qualificationName: detail.qualificationName || '',
        qualificationType: detail.qualificationType || TYPE_RICH_TEXT,
        recordNumber: detail.recordNumber || '',
        contentHtml: detail.contentHtml || '',
        linkUrl: detail.linkUrl || '',
        sort:
          detail.sort === undefined || detail.sort === null
            ? defaultValues.sort
            : String(detail.sort),
        publishEnabled: detail.publishEnabled ?? true,
      });
    } catch (error) {
      message.error((error as Error)?.message || '平台资质详情加载失败');
      setModalOpen(false);
    } finally {
      setModalLoading(false);
    }
  };
  const handleDelete = (record: QualificationItem) => {
    Modal.confirm({
      title: '确定要删除该平台资质吗?',
      okText: '删除',
      cancelText: '取消',
      onOk: async () => {
        try {
          await deletePlatformQualification(record.id);
          message.success('删除成功');
          const nextPage =
            list.length === 1 && pagination.page > 1 ? pagination.page - 1 : pagination.page;
          await loadList(nextPage, pagination.pageSize);
        } catch (error) {
          message.error((error as Error)?.message || '删除失败,请稍后重试');
          throw error;
        }
      },
    });
  };
  const handlePublishStatus = async (record: QualificationItem, publishEnabled: boolean) => {
    try {
      await updatePlatformQualificationPublishStatus(record.id, publishEnabled);
      message.success(publishEnabled ? '上架成功' : '下架成功');
      await loadList(pagination.page, pagination.pageSize);
    } catch (error) {
      message.error((error as Error)?.message || '状态更新失败,请稍后重试');
    }
  };
  const handleSubmit = async () => {
    if (isDetailMode) {
      handleCloseModal();
      return;
    }
    const values = await form.validateFields();
    if (showRichTextField && isBlankRichText(values.contentHtml)) {
      form.setFields([
        {
          name: 'contentHtml',
          errors: ['请输入资质内容'],
        },
      ]);
      return;
    }
    const payload = {
      qualificationName: values.qualificationName?.trim(),
      qualificationType: values.qualificationType,
      recordNumber: values.recordNumber?.trim(),
      contentHtml: showRichTextField ? values.contentHtml?.trim() : undefined,
      linkUrl: showLinkField ? values.linkUrl?.trim() : undefined,
      sort: Number(values.sort),
      publishEnabled: !!values.publishEnabled,
    };
    setSubmitting(true);
    try {
      if (modalMode === 'create') {
        await createPlatformQualification(payload);
        message.success('新增成功');
      } else if (editingId) {
        await updatePlatformQualification(editingId, payload);
        message.success('保存成功');
      }
      setModalOpen(false);
      form.resetFields();
      await loadList(modalMode === 'create' ? 1 : pagination.page, pagination.pageSize);
    } catch (error) {
      message.error((error as Error)?.message || '保存失败,请稍后重试');
    } finally {
      setSubmitting(false);
    }
  };
  const columns: ColumnsType<QualificationItem> = [
    {
      title: '序号',
      width: 80,
      render: (_, __, index) => (pagination.page - 1) * pagination.pageSize + index + 1,
    },
    {
      title: '资质名称',
      dataIndex: 'qualificationName',
      width: 180,
      ellipsis: true,
    },
    {
      title: '备案号',
      dataIndex: 'recordNumber',
      width: 180,
      ellipsis: true,
    },
    {
      title: '类型',
      dataIndex: 'qualificationType',
      width: 120,
      render: (value) => getTypeLabel(value),
    },
    {
      title: '添加人',
      dataIndex: 'createdBy',
      width: 120,
      render: (value) => value || '-',
    },
    {
      title: '添加时间',
      dataIndex: 'createdAt',
      width: 180,
      render: (value) => value || '-',
    },
    {
      title: '排序',
      dataIndex: 'sort',
      width: 100,
      render: (value) => (value === undefined || value === null ? '-' : value),
    },
    {
      title: '状态',
      dataIndex: 'publishEnabled',
      width: 110,
      render: (value) =>
        value ? <Tag color="blue">上架中</Tag> : <Tag color="default">已下架</Tag>,
    },
    {
      title: '操作',
      width: 240,
      render: (_, record) => (
        <Space size={0} wrap>
          {canDetail ? (
            <Button type="link" size="small" onClick={() => openModal(record, 'detail')}>
              详情
            </Button>
          ) : null}
          {canEdit ? (
            <Button type="link" size="small" onClick={() => openModal(record, 'edit')}>
              编辑
            </Button>
          ) : null}
          {canDelete ? (
            <Button type="link" size="small" onClick={() => handleDelete(record)}>
              删除
            </Button>
          ) : null}
          {canPublishStatus ? (
            <Button
              type="link"
              size="small"
              onClick={() => handlePublishStatus(record, !record.publishEnabled)}
            >
              {record.publishEnabled ? '下架' : '上架'}
            </Button>
          ) : null}
        </Space>
      ),
    },
  ];
  return (
    <div className="platform-qualification-page">
      <div className="platform-qualification-card">
        <div className="platform-qualification-card__title">平台资质</div>
        <div className="platform-qualification-search-card">
          <Form
            form={searchForm}
            layout="horizontal"
            className="platform-qualification-search-form"
          >
            <Form.Item label="资质名称" name="qualificationName">
              <Input placeholder="请输入" allowClear />
            </Form.Item>
            <div className="platform-qualification-search-form__actions">
              <Button
                onClick={async () => {
                  searchForm.resetFields();
                  await loadList(1, pagination.pageSize);
                }}
              >
                重置
              </Button>
              <Button type="primary" onClick={() => loadList(1, pagination.pageSize)}>
                搜索
              </Button>
            </div>
          </Form>
        </div>
        <div className="platform-qualification-toolbar">
          {canAdd ? (
            <Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
              新建
            </Button>
          ) : null}
        </div>
        <div className="platform-qualification-table-card">
          <Table<QualificationItem>
            rowKey="id"
            columns={columns}
            dataSource={list}
            loading={loading}
            pagination={false}
            scroll={{ x: 1320 }}
          />
          <div className="platform-qualification-pagination">
            <Pagination
              current={pagination.page}
              pageSize={pagination.pageSize}
              total={pagination.total}
              showSizeChanger
              showQuickJumper
              showTotal={(total) => `共 ${total} 条`}
              onChange={(page, pageSize) => loadList(page, pageSize)}
            />
          </div>
        </div>
      </div>
      <Modal
        open={modalOpen}
        className="platform-qualification-modal"
        destroyOnClose
        centered
        width={860}
        title={modalMode === 'create' ? '添加资质' : modalMode === 'edit' ? '编辑资质' : '资质详情'}
        onCancel={handleCloseModal}
        footer={[
          <Button key="cancel" onClick={handleCloseModal} disabled={submitting}>
            {isDetailMode ? '关闭' : '取消'}
          </Button>,
          isDetailMode ? null : (
            <Button key="submit" type="primary" loading={submitting} onClick={handleSubmit}>
              确定
            </Button>
          ),
        ]}
      >
        <Form
          form={form}
          layout="horizontal"
          className="platform-qualification-form"
          labelCol={{ span: 5 }}
          disabled={isDetailMode || modalLoading}
        >
          <div className="platform-qualification-form__body">
            <Form.Item
              label="资质名称"
              name="qualificationName"
              rules={[
                { required: true, message: '请输入资质名称' },
                { max: 64, message: '资质名称长度不能超过64个字符' },
              ]}
            >
              <Input placeholder="请输入" maxLength={64} />
            </Form.Item>
            <Form.Item
              label="类型"
              name="qualificationType"
              rules={[{ required: true, message: '请选择资质类型' }]}
            >
              <Radio.Group
                options={typeOptions}
                onChange={(event) => {
                  const nextType = event.target.value;
                  if (nextType === TYPE_RICH_TEXT) {
                    form.setFieldsValue({ linkUrl: '' });
                  } else {
                    form.setFieldsValue({ contentHtml: '' });
                  }
                }}
              />
            </Form.Item>
            <Form.Item
              label="备案号"
              name="recordNumber"
              rules={[
                { required: true, message: '请输入备案号' },
                { max: 64, message: '备案号长度不能超过64个字符' },
              ]}
            >
              <Input placeholder="请输入" maxLength={64} />
            </Form.Item>
            {showRichTextField ? (
              <Form.Item
                label="资质内容"
                name="contentHtml"
                rules={[
                  { required: true, message: '请输入资质内容' },
                  {
                    validator: async (_, value) => {
                      if (isBlankRichText(value)) {
                        throw new Error('请输入资质内容');
                      }
                    },
                  },
                ]}
              >
                <div className="platform-qualification-form__editor">
                  <BraftEditorCom
                    data={form.getFieldValue('contentHtml') || ''}
                    readOnly={isDetailMode}
                    resultCallback={(html: string) => form.setFieldsValue({ contentHtml: html })}
                  />
                </div>
              </Form.Item>
            ) : null}
            {showLinkField ? (
              <Form.Item
                label="跳转链接"
                name="linkUrl"
                rules={[
                  { required: true, message: '请输入跳转链接' },
                  { max: 512, message: '跳转链接长度不能超过512个字符' },
                ]}
              >
                <Input placeholder="请输入" maxLength={512} />
              </Form.Item>
            ) : null}
            <Form.Item
              label="排序"
              name="sort"
              rules={[{ required: true, message: '请输入排序' }, { validator: validateSort }]}
            >
              <Input placeholder="请输入" maxLength={9} />
            </Form.Item>
            <Form.Item
              label="上架状态"
              name="publishEnabled"
              rules={[{ required: true, message: '请选择上架状态' }]}
            >
              <Radio.Group
                options={[
                  { label: '上架', value: true },
                  { label: '下架', value: false },
                ]}
              />
            </Form.Item>
          </div>
        </Form>
        {modalLoading ? (
          <div className="platform-qualification-modal__loading">加载中...</div>
        ) : null}
      </Modal>
    </div>
  );
};
export default PlatformQualificationPage;
tcm-training-admin/src/pages/system/platform-qualification/service.ts
New file
@@ -0,0 +1,44 @@
import { request } from '@umijs/max';
export async function getPlatformQualificationPage(params?: Record<string, any>) {
  return request('/platform-qualifications', {
    method: 'GET',
    params,
  });
}
export async function getPlatformQualificationDetail(id: number | string) {
  return request(`/platform-qualifications/${id}`, {
    method: 'GET',
  });
}
export async function createPlatformQualification(data: Record<string, any>) {
  return request('/platform-qualifications', {
    method: 'POST',
    data,
  });
}
export async function updatePlatformQualification(id: number | string, data: Record<string, any>) {
  return request(`/platform-qualifications/${id}`, {
    method: 'PUT',
    data,
  });
}
export async function deletePlatformQualification(id: number | string) {
  return request(`/platform-qualifications/${id}`, {
    method: 'DELETE',
  });
}
export async function updatePlatformQualificationPublishStatus(
  id: number | string,
  publishEnabled: boolean,
) {
  return request(`/platform-qualifications/${id}/publish-status`, {
    method: 'PATCH',
    data: { publishEnabled },
  });
}
tcm-training-admin/src/pages/system/platform-qualification/style.less
New file
@@ -0,0 +1,152 @@
.platform-qualification-page {
  min-height: calc(100vh - 48px);
  padding: 18px 20px 24px;
  background: #f2f5f9;
}
.platform-qualification-card {
  min-height: 700px;
  padding: 8px 14px 18px;
  background: #fff;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
}
.platform-qualification-card__title {
  margin: 0 0 22px;
  color: #303133;
  font-weight: 500;
  font-size: 18px;
}
.platform-qualification-search-card {
  margin-bottom: 20px;
}
.platform-qualification-search-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0 18px;
  align-items: center;
}
.platform-qualification-search-form .ant-form-item {
  margin-bottom: 0;
}
.platform-qualification-search-form .ant-input {
  width: 170px;
}
.platform-qualification-search-form__actions {
  display: flex;
  gap: 12px;
  align-items: center;
}
.platform-qualification-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 14px;
}
.platform-qualification-table-card {
  overflow: hidden;
  border: 1px solid #e6ebf2;
  border-radius: 4px;
}
.platform-qualification-table-card .ant-table-thead > tr > th {
  color: #303133;
  font-weight: 500;
  background: #f5f7fa;
}
.platform-qualification-table-card .ant-table-tbody > tr > td {
  color: #606266;
  vertical-align: middle;
}
.platform-qualification-pagination {
  display: flex;
  justify-content: flex-end;
  padding: 16px 18px;
  background: #fff;
}
.platform-qualification-modal .ant-modal-content {
  padding: 18px 24px 20px;
  border-radius: 8px;
}
.platform-qualification-modal .ant-modal-title {
  color: #303133;
  font-weight: 500;
  font-size: 18px;
}
.platform-qualification-modal .ant-modal-body {
  padding-top: 12px;
}
.platform-qualification-form {
  position: relative;
}
.platform-qualification-form__body {
  padding-right: 6px;
}
.platform-qualification-form .ant-form-item {
  margin-bottom: 18px;
}
.platform-qualification-form .ant-form-item-label > label {
  color: #303133;
}
.platform-qualification-form__editor {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}
.platform-qualification-modal__loading {
  position: absolute;
  top: 66px;
  right: 32px;
  color: #909399;
  font-size: 12px;
}
@media (max-width: 900px) {
  .platform-qualification-page {
    padding: 12px;
  }
  .platform-qualification-card {
    min-height: auto;
    padding: 16px;
  }
  .platform-qualification-search-form {
    flex-direction: column;
    align-items: stretch;
  }
  .platform-qualification-search-form .ant-input {
    width: 100%;
  }
  .platform-qualification-search-form__actions {
    justify-content: flex-end;
  }
  .platform-qualification-modal {
    width: calc(100vw - 24px) !important;
    max-width: calc(100vw - 24px);
  }
  .platform-qualification-modal .ant-modal-content {
    padding: 16px;
  }
}
tcm-training-admin/src/pages/system/settings-redirect/index.tsx
@@ -17,6 +17,14 @@
      history.replace('/system/settings/banner');
      return;
    }
    if (access['system:activity-notice:view']) {
      history.replace('/system/settings/activity-notice');
      return;
    }
    if (access['system:platform-qualification:view']) {
      history.replace('/system/settings/platform-qualification');
      return;
    }
    if (access['system:message-push:view']) {
      history.replace('/system/settings/message-push');
      return;