hejianhao
2026-08-19 861ee7cdacd5527b82f06fe9ddb34848df089200
feat0806: 管理后台设置微赞直播间密码
10个文件已添加
5个文件已修改
368 ■■■■■ 已修改文件
tcm-training-admin-backend/sql/20260806_admin.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/controller/LiveRoomSettingController.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/LiveRoomSettingSaveRequest.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/LiveRoomSetting.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/LiveRoomSettingMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/LiveRoomSettingService.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/LiveRoomSettingVO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/config/routes.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/live/live-room-setting/index.tsx 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/live/live-room-setting/service.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/live/live-room-setting/style.less 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/home.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/service.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/20260806_admin.sql
@@ -60,3 +60,17 @@
INSERT INTO sys_menu (id, parent_id, name, menu_code, type, sort, status, visible, permission_key, remark, deleted)
VALUES (5045, 2118, '编辑', 'member_clinic_edit', 'B', 1, 1, 1, 'member:clinic:edit', '编辑诊所按钮', 0)
ON DUPLICATE KEY UPDATE permission_key = VALUES(permission_key), deleted = 0;
-- 微赞直播间单例设置:仅保留 Banner 图片地址和直播间地址两个业务字段
CREATE TABLE IF NOT EXISTS tcm_live_room_setting (
    banner_image_url VARCHAR(500) NOT NULL COMMENT 'Banner图片地址',
    live_room_url VARCHAR(500) NULL COMMENT '直播间地址'
) COMMENT='微赞直播间设置';
INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
VALUES (6000, 3050, '微赞直播间设置', 'live_room_setting', 'C', '/live/course/live-room-setting', 'Live/LiveRoomSetting', NULL, 18, 1, 1, 'live:room-setting: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;
INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
VALUES (6001, 6000, '编辑微赞直播间设置', 'live_room_setting_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:room-setting: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/app/home/controller/AppLiveRoomSettingController.java
New file
@@ -0,0 +1,23 @@
package com.tcm.training.admin.app.home.controller;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.live.service.LiveRoomSettingService;
import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping({"/api/app/home", "/api/v1/app/home"})
public class AppLiveRoomSettingController {
    private final LiveRoomSettingService service;
    public AppLiveRoomSettingController(LiveRoomSettingService service) {
        this.service = service;
    }
    @GetMapping("/live-room-setting")
    public ApiResponse<LiveRoomSettingVO> current() {
        return ApiResponse.ok(service.current());
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
@@ -269,7 +269,10 @@
                null, null, null, 16, "live:session:delete", "直播场次删除按钮"));
        menuIds.add(saveMenu(3077L, 3060L, "推送开播提醒", "live_session_push_notification", "B",
                null, null, null, 17, "live:session:push-notification", "直播场次推送提醒按钮"));
        menuIds.add(saveMenu(3080L, 3050L, "直播预约记录", "live_reserve_view", "C",
        menuIds.add(saveMenu(6000L, 3050L, "微赞直播间设置", "live_room_setting", "C",
                "/live/course/live-room-setting", "Live/LiveRoomSetting", null, 18, "live:room-setting:view", "微赞直播间设置菜单"));
        menuIds.add(saveMenu(6001L, 6000L, "编辑微赞直播间设置", "live_room_setting_edit", "B",
                null, null, null, 1, "live:room-setting:edit", "微赞直播间设置编辑按钮"));        menuIds.add(saveMenu(3080L, 3050L, "直播预约记录", "live_reserve_view", "C",
                "/live/course/reserve-records", "Live/ReserveRecords", null, 20, "live:reserve:view", "直播预约记录菜单"));
        menuIds.add(saveMenu(3081L, 3050L, "直播观看记录", "live_watch_view", "C",
                "/live/course/watch-records", "Live/WatchRecords", null, 21, "live:watch:view", "直播观看记录菜单"));
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/controller/LiveRoomSettingController.java
New file
@@ -0,0 +1,35 @@
package com.tcm.training.admin.live.controller;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.live.dto.LiveRoomSettingSaveRequest;
import com.tcm.training.admin.live.service.LiveRoomSettingService;
import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
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.RestController;
import javax.validation.Valid;
@Validated
@RestController
@RequestMapping({"/api/v1", "/api"})
public class LiveRoomSettingController {
    private final LiveRoomSettingService service;
    public LiveRoomSettingController(LiveRoomSettingService service) {
        this.service = service;
    }
    @GetMapping("/live-room-settings/current")
    public ApiResponse<LiveRoomSettingVO> current() {
        return ApiResponse.ok(service.current());
    }
    @PutMapping("/live-room-settings/current")
    public ApiResponse<LiveRoomSettingVO> save(@Valid @RequestBody LiveRoomSettingSaveRequest request) {
        return ApiResponse.ok(service.save(request));
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/LiveRoomSettingSaveRequest.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.NotBlank;
import javax.validation.constraints.Size;
@Getter
@Setter
@NoArgsConstructor
public class LiveRoomSettingSaveRequest {
    @NotBlank(message = "请上传直播间 Banner 图片")
    @Size(max = 500, message = "Banner 图片地址不能超过500个字符")
    private String bannerImageUrl;
    @Size(max = 500, message = "直播间地址不能超过500个字符")
    private String liveRoomUrl;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/LiveRoomSetting.java
New file
@@ -0,0 +1,19 @@
package com.tcm.training.admin.live.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@TableName("tcm_live_room_setting")
@Getter
@Setter
@NoArgsConstructor
public class LiveRoomSetting {
    @TableField("banner_image_url")
    private String bannerImageUrl;
    @TableField("live_room_url")
    private String liveRoomUrl;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/LiveRoomSettingMapper.java
New file
@@ -0,0 +1,19 @@
package com.tcm.training.admin.live.mapper;
import com.tcm.training.admin.live.entity.LiveRoomSetting;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface LiveRoomSettingMapper {
    @Select("SELECT banner_image_url, live_room_url FROM tcm_live_room_setting LIMIT 1")
    LiveRoomSetting selectCurrent();
    @Insert("INSERT INTO tcm_live_room_setting (banner_image_url, live_room_url) VALUES (#{bannerImageUrl}, #{liveRoomUrl})")
    int insertCurrent(LiveRoomSetting setting);
    @Update("UPDATE tcm_live_room_setting SET banner_image_url = #{bannerImageUrl}, live_room_url = #{liveRoomUrl}")
    int updateCurrent(LiveRoomSetting setting);
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/LiveRoomSettingService.java
New file
@@ -0,0 +1,51 @@
package com.tcm.training.admin.live.service;
import com.tcm.training.admin.live.dto.LiveRoomSettingSaveRequest;
import com.tcm.training.admin.live.entity.LiveRoomSetting;
import com.tcm.training.admin.live.mapper.LiveRoomSettingMapper;
import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LiveRoomSettingService {
    private final LiveRoomSettingMapper mapper;
    public LiveRoomSettingService(LiveRoomSettingMapper mapper) {
        this.mapper = mapper;
    }
    public LiveRoomSettingVO current() {
        return toVO(mapper.selectCurrent());
    }
    @Transactional
    public LiveRoomSettingVO save(LiveRoomSettingSaveRequest request) {
        LiveRoomSetting setting = mapper.selectCurrent();
        if (setting == null) {
            setting = new LiveRoomSetting();
            fill(setting, request);
            mapper.insertCurrent(setting);
        } else {
            fill(setting, request);
            mapper.updateCurrent(setting);
        }
        return toVO(setting);
    }
    private void fill(LiveRoomSetting setting, LiveRoomSettingSaveRequest request) {
        setting.setBannerImageUrl(request.getBannerImageUrl().trim());
        String liveRoomUrl = request.getLiveRoomUrl();
        setting.setLiveRoomUrl(liveRoomUrl == null || liveRoomUrl.trim().isEmpty() ? null : liveRoomUrl.trim());
    }
    private LiveRoomSettingVO toVO(LiveRoomSetting setting) {
        if (setting == null) {
            return null;
        }
        LiveRoomSettingVO vo = new LiveRoomSettingVO();
        vo.setBannerImageUrl(setting.getBannerImageUrl());
        vo.setLiveRoomUrl(setting.getLiveRoomUrl());
        return vo;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/LiveRoomSettingVO.java
New file
@@ -0,0 +1,13 @@
package com.tcm.training.admin.live.vo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class LiveRoomSettingVO {
    private String bannerImageUrl;
    private String liveRoomUrl;
}
tcm-training-admin/config/routes.ts
@@ -314,6 +314,11 @@
            component: './live/broadcast/edit',
          },
          {
            name: '微赞直播间设置',
            path: '/live/course/live-room-setting',
            access: 'live:room-setting:view',
            component: './live/live-room-setting',
          },          {
            name: '直播预约记录',
            path: '/live/course/reserve-records',
            access: 'live:reserve:view',
tcm-training-admin/src/pages/live/live-room-setting/index.tsx
New file
@@ -0,0 +1,107 @@
import { PlusOutlined } from '@ant-design/icons';
import { history, useAccess } from '@umijs/max';
import { Button, Form, Image, Input, Spin, Upload, message } from 'antd';
import React, { useEffect, useState } from 'react';
import { getLiveRoomSetting, saveLiveRoomSetting, uploadLiveRoomBanner } from './service';
import './style.less';
type FormValues = { bannerImageUrl?: string; liveRoomUrl?: string };
const LiveRoomSettingPage: React.FC = () => {
  const access = useAccess();
  const [form] = Form.useForm<FormValues>();
  const [loading, setLoading] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [uploading, setUploading] = useState(false);
  const canView = access['live:room-setting:view'];
  const canEdit = access['live:room-setting:edit'];
  const bannerImageUrl = Form.useWatch('bannerImageUrl', form);
  const loadCurrent = async () => {
    setLoading(true);
    try {
      const res = await getLiveRoomSetting();
      form.setFieldsValue({
        bannerImageUrl: res.data?.bannerImageUrl || '',
        liveRoomUrl: res.data?.liveRoomUrl || '',
      });
    } catch (error) {
      message.error((error as Error)?.message || '直播间设置加载失败');
    } finally {
      setLoading(false);
    }
  };
  useEffect(() => {
    if (!canView) {
      history.replace('/Welcome');
      return;
    }
    loadCurrent();
  }, [canView]);
  const handleSubmit = async () => {
    if (!canEdit || submitting) return;
    const values = await form.validateFields();
    setSubmitting(true);
    try {
      await saveLiveRoomSetting({
        bannerImageUrl: values.bannerImageUrl!,
        liveRoomUrl: values.liveRoomUrl?.trim() || undefined,
      });
      message.success('保存成功');
      await loadCurrent();
    } catch (error) {
      message.error((error as Error)?.message || '保存失败,请稍后重试');
    } finally {
      setSubmitting(false);
    }
  };
  return (
    <div className="live-room-setting-page">
      <div className="live-room-setting-card">
        <div className="live-room-setting-title">微赞直播间设置</div>
        <Spin spinning={loading}>
          <Form form={form} layout="vertical" disabled={loading || !canEdit} className="live-room-setting-form">
            <Form.Item label="Banner图片" name="bannerImageUrl" rules={[{ required: true, message: '请上传Banner图片' }]}>
              <Input type="hidden" />
            </Form.Item>
            <div className="live-room-setting-upload-row">
              {bannerImageUrl ? <Image src={bannerImageUrl} className="live-room-setting-preview" /> : null}
              <Upload
                accept="image/*"
                showUploadList={false}
                customRequest={async (options) => {
                  setUploading(true);
                  try {
                    const res = await uploadLiveRoomBanner(options.file as File);
                    const url = res.data?.url;
                    if (!url) throw new Error('上传失败');
                    form.setFieldValue('bannerImageUrl', url);
                    await form.validateFields(['bannerImageUrl']);
                    options.onSuccess?.(res);
                  } catch (error) {
                    message.error((error as Error)?.message || '图片上传失败');
                    options.onError?.(error as Error);
                  } finally {
                    setUploading(false);
                  }
                }}
              >
                <Button icon={<PlusOutlined />} loading={uploading}>上传图片</Button>
              </Upload>
              <span className="live-room-setting-tip">建议尺寸 750 × 320px,支持 JPG、PNG</span>
            </div>
            <Form.Item label="直播间地址" name="liveRoomUrl" rules={[{ type: 'url', message: '请输入正确的直播间地址' }, { max: 500 }]}>
              <Input placeholder="请输入微赞直播间地址(可不填)" allowClear />
            </Form.Item>
            <Button type="primary" loading={submitting} disabled={!canEdit} onClick={handleSubmit}>保存</Button>
          </Form>
        </Spin>
      </div>
    </div>
  );
};
export default LiveRoomSettingPage;
tcm-training-admin/src/pages/live/live-room-setting/service.ts
New file
@@ -0,0 +1,15 @@
import { request } from '@umijs/max';
export async function getLiveRoomSetting() {
  return request('/live-room-settings/current', { method: 'GET' });
}
export async function saveLiveRoomSetting(data: { bannerImageUrl: string; liveRoomUrl?: string }) {
  return request('/live-room-settings/current', { method: 'PUT', data });
}
export async function uploadLiveRoomBanner(file: File) {
  const formData = new FormData();
  formData.append('file', file);
  return request('/upload/image', { method: 'POST', data: formData });
}
tcm-training-admin/src/pages/live/live-room-setting/style.less
New file
@@ -0,0 +1,8 @@
.live-room-setting-page { min-height: calc(100vh - 48px); padding: 20px; background: #f2f5f9; }
.live-room-setting-card { min-height: 620px; padding: 24px; border-radius: 10px; background: #fff; box-shadow: 0 2px 10px rgba(15, 23, 42, .04); }
.live-room-setting-title { margin-bottom: 28px; color: #303133; font-size: 18px; font-weight: 500; }
.live-room-setting-form { width: 620px; }
.live-room-setting-upload-row { display: flex; align-items: center; gap: 16px; margin: -14px 0 28px; }
.live-room-setting-preview { width: 300px; height: 128px; border-radius: 8px; object-fit: cover; border: 1px solid #ebeef5; }
.live-room-setting-tip { color: #909399; font-size: 13px; }
@media (max-width: 800px) { .live-room-setting-form { width: 100%; } .live-room-setting-upload-row { align-items: flex-start; flex-direction: column; } }
traditionalApp/pages/home/home.vue
@@ -43,7 +43,7 @@
            <view class="quick-entry-row">
                <view class="quick-entry-card" hover-class="quick-entry-card--pressed" hover-stay-time="80"
                    @tap="goWeiZanLive">
                    <image src="/static/image/home/weizan-live.png" mode="aspectFill" class="quick-entry-card__img"></image>
                    <image :src="weiZanLiveBanner || '/static/image/home/weizan-live.png'" mode="aspectFill" class="quick-entry-card__img"></image>
                </view>
                <view class="quick-entry-card" hover-class="quick-entry-card--pressed" hover-stay-time="80"
                    @tap="goOperatorTraining">
@@ -141,7 +141,7 @@
<script setup lang="ts">
    import { computed, ref, onMounted } from "vue";
    import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
    import { getBannerList, getHomeFeaturedContent, getHomeLiveCourseList } from "./service";
    import { getBannerList, getHomeFeaturedContent, getHomeLiveCourseList, getLiveRoomSetting } from "./service";
    interface HomeBannerItem {
        id : number | string;
@@ -201,7 +201,8 @@
        const systemInfo = uni.getSystemInfoSync();
        return String(systemInfo.platform || '').toLowerCase() === 'ios';
    });
    const weiZanLiveH5Url = 'https://www.vzan.com';
    const weiZanLiveH5Url = ref('');
    const weiZanLiveBanner = ref('');
    const operatorTrainingLiveCourse = computed(() => {
        const list = liveCourseList.value;
@@ -254,6 +255,18 @@
    /**
     * 请求首页顶部 banner 列表。
     */
    const loadLiveRoomSetting = async () => {
        try {
            const res = await getLiveRoomSetting();
            if (res?.code === 0 && res.data) {
                weiZanLiveBanner.value = res.data.bannerImageUrl || '';
                weiZanLiveH5Url.value = res.data.liveRoomUrl || '';
            }
        } catch {
            weiZanLiveBanner.value = '';
            weiZanLiveH5Url.value = '';
        }
    };
    const loadBannerList = async () => {
        try {
            const res = await getBannerList();
@@ -315,11 +328,15 @@
    }
    /**
     * 跳转微赞直播 H5 页面(后端接口暂未提供,先使用静态链接)。
     * 跳转后台配置的微赞直播 H5 页面。
     */
    const goWeiZanLive = () => {
        if (!weiZanLiveH5Url.value) {
            uni.showToast({ title: '直播间地址暂未配置', icon: 'none' });
            return;
        }
        uni.navigateTo({
            url: `/pages/webview/webview?url=${encodeURIComponent(weiZanLiveH5Url)}&title=${encodeURIComponent('微赞直播')}`
            url: `/pages/webview/webview?url=${encodeURIComponent(weiZanLiveH5Url.value)}&title=${encodeURIComponent('微赞直播')}`
        });
    };
@@ -399,6 +416,7 @@
        try {
            await Promise.allSettled([
                loadBannerList(),
                loadLiveRoomSetting(),
                loadHomeLiveCourseList(),
                loadFeaturedContent()
            ]);
traditionalApp/pages/home/service.js
@@ -42,3 +42,9 @@
export const getNoticeList = (params) => {
    return request.get(`/index/getNoticeList`, params, 1)
}
/**
 * 获取 APP 首页微赞直播间配置。
 */
export const getLiveRoomSetting = () => {
    return publicGet('/api/app/home/live-room-setting')
}