| | |
| | | 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; |
| | |
| | | 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", |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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> { |
| | | } |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | public boolean verifyPassword(LiveCoursePasswordVerifyRequest req) { |
| | | if (req.getType() != LIVE_ROOM_PASSWORD_TYPE) { |
| | | if (req.getType() == LIVE_ROOM_PASSWORD_TYPE) { |
| | | // 直播间密码验证 |
| | | LiveCourse course = liveCourseMapper.selectOne(new LambdaQueryWrapper<LiveCourse>() |
| | | .isNotNull(LiveCourse::getPassword) |
| | | .orderByAsc(LiveCourse::getId) |
| | | .last("limit 1")); |
| | | return course != null |
| | | && 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("不支持的密码类型"); |
| | | } |
| | | LiveCourse course = liveCourseMapper.selectOne(new LambdaQueryWrapper<LiveCourse>() |
| | | .isNotNull(LiveCourse::getPassword) |
| | | .orderByAsc(LiveCourse::getId) |
| | | .last("limit 1")); |
| | | return course != null |
| | | && course.getPassword() != null |
| | | && !course.getPassword().isEmpty() |
| | | && passwordEncoder.matches(req.getPassword(), course.getPassword()); |
| | | } |
| | | |
| | | @Transactional |
| New file |
| | |
| | | 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()); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | |
| | | 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', |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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, |
| | | }); |
| | | } |