hejianhao
2026-08-20 8e2c06d3cf82be192505a02af654cdde9d4723a5
feat0806: 项目介绍密码功能
8个文件已添加
4个文件已修改
470 ■■■■■ 已修改文件
tcm-training-admin-backend/sql/20260806_admin.sql 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/controller/ProjectIntroductionPasswordController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/ProjectIntroductionPasswordRequest.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/ProjectIntroductionPassword.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/ProjectIntroductionPasswordMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/LiveCourseAppService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/ProjectIntroductionPasswordService.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/ProjectIntroductionPasswordVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/config/routes.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/live/project-introduction-password/index.tsx 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/services/project-introduction-password.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/20260806_admin.sql
@@ -124,3 +124,25 @@
  KEY `idx_status` (`status`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目介绍表';
-- ----------------------------
-- 项目介绍密码配置表
-- ----------------------------
CREATE TABLE IF NOT EXISTS `tcm_project_introduction_password` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `password_enabled` TINYINT NOT NULL DEFAULT 0 COMMENT '是否启用密码访问:1启用 0不启用',
  `password` VARCHAR(255) DEFAULT NULL COMMENT '访问密码(加密)',
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目介绍密码配置表';
-- 项目介绍密码菜单(id: 6014)
INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
VALUES (6014, 3050, '项目介绍密码', 'live_project_introduction_password', 'C', '/live/course/project-introduction-password', 'Live/ProjectIntroductionPassword', NULL, 61, 1, 1, 'live:project-introduction-password:view', '项目介绍密码菜单', 0)
ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), path = VALUES(path), component = VALUES(component), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0;
-- 编辑项目介绍密码按钮(id: 6015)
INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
VALUES (6015, 6014, '编辑项目介绍密码', 'live_project_introduction_password_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:project-introduction-password:edit', '项目介绍密码编辑按钮', 0)
ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0;
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
@@ -332,6 +332,10 @@
                null, null, null, 2, "live:project-introduction:edit", "项目介绍编辑按钮"));
        menuIds.add(saveMenu(6013L, 6010L, "删除项目介绍", "live_project_introduction_delete", "B",
                null, null, null, 3, "live:project-introduction:delete", "项目介绍删除按钮"));
        menuIds.add(saveMenu(6014L, 3050L, "项目介绍密码", "live_project_introduction_password", "C",
                "/live/course/project-introduction-password", "Live/ProjectIntroductionPassword", null, 61, "live:project-introduction-password:view", "项目介绍密码菜单"));
        menuIds.add(saveMenu(6015L, 6014L, "编辑项目介绍密码", "live_project_introduction_password_edit", "B",
                null, null, null, 1, "live:project-introduction-password:edit", "项目介绍密码编辑按钮"));
        menuIds.add(saveMenu(4000L, 0L, "交易与财务", "trade_root", "M",
                "/trade", null, "moneyCollect", 400, null, "交易与财务模块"));
        menuIds.add(saveMenu(4100L, 4000L, "订单", "trade_order_group", "M",
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/controller/ProjectIntroductionPasswordController.java
New file
@@ -0,0 +1,42 @@
package com.tcm.training.admin.live.controller;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.live.dto.ProjectIntroductionPasswordRequest;
import com.tcm.training.admin.live.service.ProjectIntroductionPasswordService;
import com.tcm.training.admin.live.vo.ProjectIntroductionPasswordVO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
 * 项目介绍密码管理接口
 */
@Validated
@RestController
@RequestMapping("/api/admin/live/project-introduction-password")
public class ProjectIntroductionPasswordController {
    private final ProjectIntroductionPasswordService passwordService;
    public ProjectIntroductionPasswordController(ProjectIntroductionPasswordService passwordService) {
        this.passwordService = passwordService;
    }
    /**
     * 获取项目介绍密码配置
     */
    @GetMapping
    public ApiResponse<ProjectIntroductionPasswordVO> getPasswordConfig() {
        return ApiResponse.ok(passwordService.getPasswordConfig());
    }
    /**
     * 保存项目介绍密码配置
     */
    @PostMapping
    public ApiResponse<Void> savePasswordConfig(@Valid @RequestBody ProjectIntroductionPasswordRequest req) {
        passwordService.savePasswordConfig(req);
        return ApiResponse.ok(null);
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/ProjectIntroductionPasswordRequest.java
New file
@@ -0,0 +1,20 @@
package com.tcm.training.admin.live.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Getter
@Setter
@NoArgsConstructor
public class ProjectIntroductionPasswordRequest {
    @NotNull(message = "是否启用密码访问不能为空")
    private Integer passwordEnabled;
    @Size(max = 6, message = "密码不能超过6个字符")
    private String password;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/ProjectIntroductionPassword.java
New file
@@ -0,0 +1,32 @@
package com.tcm.training.admin.live.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@TableName("tcm_project_introduction_password")
public class ProjectIntroductionPassword {
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 是否启用密码访问:1启用 0不启用
     */
    private Integer passwordEnabled;
    /**
     * 访问密码(加密)
     */
    private String password;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/ProjectIntroductionPasswordMapper.java
New file
@@ -0,0 +1,9 @@
package com.tcm.training.admin.live.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tcm.training.admin.live.entity.ProjectIntroductionPassword;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProjectIntroductionPasswordMapper extends BaseMapper<ProjectIntroductionPassword> {
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/LiveCourseAppService.java
@@ -35,23 +35,27 @@
public class LiveCourseAppService {
    private static final int LIVE_ROOM_PASSWORD_TYPE = 1;
    private static final int PROJECT_INTRODUCTION_PASSWORD_TYPE = 2;
    private final LiveCourseMapper liveCourseMapper;
    private final ExpertMapper expertMapper;
    private final LiveSessionMapper liveSessionMapper;
    private final LiveSessionQuestionMapper liveSessionQuestionMapper;
    private final BCryptPasswordEncoder passwordEncoder;
    private final ProjectIntroductionPasswordService projectIntroductionPasswordService;
    public LiveCourseAppService(LiveCourseMapper liveCourseMapper,
                                ExpertMapper expertMapper,
                                LiveSessionMapper liveSessionMapper,
                                LiveSessionQuestionMapper liveSessionQuestionMapper,
                                BCryptPasswordEncoder passwordEncoder) {
                                BCryptPasswordEncoder passwordEncoder,
                                ProjectIntroductionPasswordService projectIntroductionPasswordService) {
        this.liveCourseMapper = liveCourseMapper;
        this.expertMapper = expertMapper;
        this.liveSessionMapper = liveSessionMapper;
        this.liveSessionQuestionMapper = liveSessionQuestionMapper;
        this.passwordEncoder = passwordEncoder;
        this.projectIntroductionPasswordService = projectIntroductionPasswordService;
    }
    public PageResponse<LiveCourseListItemVO> page(int page, int pageSize, String keyword, Long expertId, Integer status, Integer liveStatus) {
@@ -186,9 +190,8 @@
    }
    public boolean verifyPassword(LiveCoursePasswordVerifyRequest req) {
        if (req.getType() != LIVE_ROOM_PASSWORD_TYPE) {
            throw new IllegalArgumentException("不支持的密码类型");
        }
        if (req.getType() == LIVE_ROOM_PASSWORD_TYPE) {
            // 直播间密码验证
        LiveCourse course = liveCourseMapper.selectOne(new LambdaQueryWrapper<LiveCourse>()
                .isNotNull(LiveCourse::getPassword)
                .orderByAsc(LiveCourse::getId)
@@ -197,6 +200,12 @@
                && course.getPassword() != null
                && !course.getPassword().isEmpty()
                && passwordEncoder.matches(req.getPassword(), course.getPassword());
        } else if (req.getType() == PROJECT_INTRODUCTION_PASSWORD_TYPE) {
            // 项目介绍密码验证
            return projectIntroductionPasswordService.verifyPassword(req.getPassword());
        } else {
            throw new IllegalArgumentException("不支持的密码类型");
        }
    }
    @Transactional
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/ProjectIntroductionPasswordService.java
New file
@@ -0,0 +1,114 @@
package com.tcm.training.admin.live.service;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tcm.training.admin.live.dto.ProjectIntroductionPasswordRequest;
import com.tcm.training.admin.live.entity.ProjectIntroductionPassword;
import com.tcm.training.admin.live.mapper.ProjectIntroductionPasswordMapper;
import com.tcm.training.admin.live.vo.ProjectIntroductionPasswordVO;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProjectIntroductionPasswordService {
    private final ProjectIntroductionPasswordMapper passwordMapper;
    private final BCryptPasswordEncoder passwordEncoder;
    public ProjectIntroductionPasswordService(ProjectIntroductionPasswordMapper passwordMapper,
                                               BCryptPasswordEncoder passwordEncoder) {
        this.passwordMapper = passwordMapper;
        this.passwordEncoder = passwordEncoder;
    }
    /**
     * 获取项目介绍密码配置
     */
    public ProjectIntroductionPasswordVO getPasswordConfig() {
        ProjectIntroductionPassword config = passwordMapper.selectOne(
                new LambdaQueryWrapper<ProjectIntroductionPassword>()
                        .orderByAsc(ProjectIntroductionPassword::getId)
                        .last("limit 1")
        );
        ProjectIntroductionPasswordVO vo = new ProjectIntroductionPasswordVO();
        if (config != null) {
            vo.setId(config.getId());
            vo.setPasswordEnabled(config.getPasswordEnabled());
            // 密码不返回明文,前端只需要知道是否设置了密码
            vo.setPassword(StrUtil.isNotBlank(config.getPassword()) ? "******" : null);
        } else {
            vo.setPasswordEnabled(0);
            vo.setPassword(null);
        }
        return vo;
    }
    /**
     * 保存项目介绍密码配置
     */
    @Transactional
    public void savePasswordConfig(ProjectIntroductionPasswordRequest req) {
        // 验证密码格式
        if (req.getPasswordEnabled() == 1) {
            if (StrUtil.isBlank(req.getPassword())) {
                throw new IllegalArgumentException("启用密码访问时,密码不能为空");
            }
            if (req.getPassword().length() != 6) {
                throw new IllegalArgumentException("密码必须是6个字符");
            }
        }
        // 查询是否已存在配置
        ProjectIntroductionPassword config = passwordMapper.selectOne(
                new LambdaQueryWrapper<ProjectIntroductionPassword>()
                        .orderByAsc(ProjectIntroductionPassword::getId)
                        .last("limit 1")
        );
        if (config == null) {
            // 新增
            config = new ProjectIntroductionPassword();
            config.setPasswordEnabled(req.getPasswordEnabled());
            if (req.getPasswordEnabled() == 1 && StrUtil.isNotBlank(req.getPassword())) {
                config.setPassword(passwordEncoder.encode(req.getPassword()));
            } else {
                config.setPassword(null);
            }
            passwordMapper.insert(config);
        } else {
            // 更新
            config.setPasswordEnabled(req.getPasswordEnabled());
            if (req.getPasswordEnabled() == 1 && StrUtil.isNotBlank(req.getPassword())) {
                config.setPassword(passwordEncoder.encode(req.getPassword()));
            } else {
                // 不启用密码时,清空密码
                config.setPassword(null);
            }
            passwordMapper.updateById(config);
        }
    }
    /**
     * 验证项目介绍密码
     */
    public boolean verifyPassword(String inputPassword) {
        if (StrUtil.isBlank(inputPassword)) {
            return false;
        }
        ProjectIntroductionPassword config = passwordMapper.selectOne(
                new LambdaQueryWrapper<ProjectIntroductionPassword>()
                        .eq(ProjectIntroductionPassword::getPasswordEnabled, 1)
                        .isNotNull(ProjectIntroductionPassword::getPassword)
                        .orderByAsc(ProjectIntroductionPassword::getId)
                        .last("limit 1")
        );
        return config != null
                && config.getPassword() != null
                && !config.getPassword().isEmpty()
                && passwordEncoder.matches(inputPassword, config.getPassword());
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/ProjectIntroductionPasswordVO.java
New file
@@ -0,0 +1,23 @@
package com.tcm.training.admin.live.vo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class ProjectIntroductionPasswordVO {
    private Long id;
    /**
     * 是否启用密码访问:1启用 0不启用
     */
    private Integer passwordEnabled;
    /**
     * 访问密码(明文,仅用于前端展示)
     */
    private String password;
}
tcm-training-admin/config/routes.ts
@@ -383,6 +383,12 @@
            component: './live/project-introduction/manage',
          },
          {
            name: '项目介绍密码',
            path: '/live/course/project-introduction-password',
            access: 'live:project-introduction:view',
            component: './live/project-introduction-password',
          },
          {
            name: '视频与课程资源管理',
            path: '/live/course/video-resources',
            access: 'live:health-course:view',
tcm-training-admin/src/pages/live/project-introduction-password/index.tsx
New file
@@ -0,0 +1,149 @@
import React, { useEffect, useState } from 'react';
import { PageContainer } from '@ant-design/pro-components';
import { Card, Form, Switch, Input, Button, message, Space } from 'antd';
import {
  getProjectIntroductionPassword,
  saveProjectIntroductionPassword,
  ProjectIntroductionPasswordConfig,
} from '@/services/project-introduction-password';
const ProjectIntroductionPassword: React.FC = () => {
  const [form] = Form.useForm();
  const [loading, setLoading] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [passwordEnabled, setPasswordEnabled] = useState(false);
  // 加载配置
  const loadConfig = async () => {
    setLoading(true);
    try {
      const res = await getProjectIntroductionPassword();
      if (res.code === 200 && res.data) {
        const enabled = res.data.passwordEnabled === 1;
        setPasswordEnabled(enabled);
        form.setFieldsValue({
          passwordEnabled: enabled,
          password: res.data.password === '******' ? '' : res.data.password,
        });
      }
    } catch (error) {
      message.error('加载配置失败');
    } finally {
      setLoading(false);
    }
  };
  useEffect(() => {
    loadConfig();
  }, []);
  // 提交保存
  const handleSubmit = async () => {
    try {
      const values = await form.validateFields();
      // 验证:启用密码时必须填写密码
      if (values.passwordEnabled && !values.password) {
        message.error('启用密码访问时,密码不能为空');
        return;
      }
      // 验证:密码必须是6位
      if (values.passwordEnabled && values.password && values.password.length !== 6) {
        message.error('密码必须是6个字符');
        return;
      }
      setSubmitting(true);
      const data: ProjectIntroductionPasswordConfig = {
        passwordEnabled: values.passwordEnabled ? 1 : 0,
        password: values.passwordEnabled ? values.password : undefined,
      };
      const res = await saveProjectIntroductionPassword(data);
      if (res.code === 200) {
        message.success('保存成功');
        loadConfig();
      } else {
        message.error(res.message || '保存失败');
      }
    } catch (error) {
      console.error('保存失败:', error);
    } finally {
      setSubmitting(false);
    }
  };
  return (
    <PageContainer
      title="项目介绍密码"
      loading={loading}
    >
      <Card>
        <Form
          form={form}
          layout="vertical"
          initialValues={{
            passwordEnabled: false,
            password: '',
          }}
          style={{ maxWidth: 600 }}
        >
          <Form.Item
            label="是否启用密码访问"
            name="passwordEnabled"
            required
            valuePropName="checked"
          >
            <Switch
              checkedChildren="启用"
              unCheckedChildren="不启用"
              onChange={(checked) => {
                setPasswordEnabled(checked);
                if (!checked) {
                  // 关闭密码访问时,清空密码字段
                  form.setFieldsValue({ password: '' });
                }
              }}
            />
          </Form.Item>
          {passwordEnabled && (
            <Form.Item
              label="访问密码"
              name="password"
              rules={[
                { required: true, message: '请输入访问密码' },
                { len: 6, message: '密码必须是6个字符' },
              ]}
              extra="密码必须是6个字符"
            >
              <Input.Password
                placeholder="请输入6位密码"
                maxLength={6}
                autoComplete="new-password"
              />
            </Form.Item>
          )}
          <Form.Item>
            <Space>
              <Button
                type="primary"
                onClick={handleSubmit}
                loading={submitting}
              >
                保存
              </Button>
              <Button onClick={loadConfig}>
                重置
              </Button>
            </Space>
          </Form.Item>
        </Form>
      </Card>
    </PageContainer>
  );
};
export default ProjectIntroductionPassword;
tcm-training-admin/src/services/project-introduction-password.ts
New file
@@ -0,0 +1,32 @@
import { request } from '@umijs/max';
/**
 * 项目介绍密码配置
 */
export interface ProjectIntroductionPasswordConfig {
  id?: number;
  passwordEnabled: number; // 是否启用密码:1启用 0不启用
  password?: string;
}
/**
 * 获取项目介绍密码配置
 */
export async function getProjectIntroductionPassword() {
  return request<API.Response<ProjectIntroductionPasswordConfig>>(
    '/api/admin/live/project-introduction-password',
    {
      method: 'GET',
    },
  );
}
/**
 * 保存项目介绍密码配置
 */
export async function saveProjectIntroductionPassword(data: ProjectIntroductionPasswordConfig) {
  return request<API.Response<void>>('/api/admin/live/project-introduction-password', {
    method: 'POST',
    data,
  });
}