hejianhao
2026-08-20 fababb22599c8a4b3034bdfc98ee7502aa73bf4d
feat0806: 动态生成微赞URL,传递门店电话
1个文件已添加
6个文件已修改
113 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/dto/CreateLiveRoomEntryRequest.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/config/routes.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/home.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/service.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/mine/mine.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/controller/AppLiveRoomSettingController.java
@@ -1,5 +1,6 @@
package com.tcm.training.admin.app.home.controller;
import com.tcm.training.admin.app.home.dto.CreateLiveRoomEntryRequest;
import com.tcm.training.admin.app.home.service.AppVzanLiveEntryService;
import com.tcm.training.admin.app.home.vo.AppVzanLiveEntryVO;
import com.tcm.training.admin.common.ApiResponse;
@@ -7,8 +8,11 @@
import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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;
@RestController
@RequestMapping({"/api/app/home", "/api/v1/app/home"})
@@ -28,7 +32,7 @@
    }
    @PostMapping("/live-room-entry")
    public ApiResponse<AppVzanLiveEntryVO> createEntry() {
        return ApiResponse.ok(vzanLiveEntryService.createEntry());
    public ApiResponse<AppVzanLiveEntryVO> createEntry(@Valid @RequestBody CreateLiveRoomEntryRequest request) {
        return ApiResponse.ok(vzanLiveEntryService.createEntry(request.getUserId()));
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/dto/CreateLiveRoomEntryRequest.java
New file
@@ -0,0 +1,17 @@
package com.tcm.training.admin.app.home.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
 * 创建微赞直播入口请求参数。
 */
@Data
public class CreateLiveRoomEntryRequest {
    /**
     * 用户ID
     */
    @NotNull(message = "用户ID不能为空")
    private Long userId;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java
@@ -14,6 +14,8 @@
import com.tcm.training.admin.live.mapper.ClinicMapper;
import com.tcm.training.admin.live.service.LiveRoomSettingService;
import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
@@ -25,34 +27,37 @@
@Service
public class AppVzanLiveEntryService {
    private static final String VZAN_AUTH_SECRET = "2b4fefe91d8cba50";
    private static final int USER_STATUS_ENABLED = 1;
    private final LiveRoomSettingService liveRoomSettingService;
    private final AppUserCenterService appUserCenterService;
    private final UserMapper userMapper;
    private final ClinicMapper clinicMapper;
    private final ProvincialAgentMapper provincialAgentMapper;
    public AppVzanLiveEntryService(LiveRoomSettingService liveRoomSettingService,
                                   AppUserCenterService appUserCenterService,
                                   UserMapper userMapper,
                                   ClinicMapper clinicMapper,
                                   ProvincialAgentMapper provincialAgentMapper) {
        this.liveRoomSettingService = liveRoomSettingService;
        this.appUserCenterService = appUserCenterService;
        this.userMapper = userMapper;
        this.clinicMapper = clinicMapper;
        this.provincialAgentMapper = provincialAgentMapper;
    }
    public AppVzanLiveEntryVO createEntry() {
    public AppVzanLiveEntryVO createEntry(Long userId) {
        LiveRoomSettingVO setting = liveRoomSettingService.current();
        String liveRoomUrl = setting == null ? null : StrUtil.trim(setting.getLiveRoomUrl());
        validateLiveRoomUrl(liveRoomUrl);
        AppUserCenterVO user = appUserCenterService.getCurrentUserCenter();
        AppUser user = requireEnabledUser(userId);
        String followPhone = resolveFollowPhone(user.getClinicId());
        String avatar = resolveAvatar(user);
        JSONObject payload = new JSONObject();
        payload.set("tuid", user.getId());
        payload.set("followPhone", followPhone);
        payload.set("third_ts", System.currentTimeMillis());
        payload.set("avatar", StrUtil.trim(user.getAvatar()));
        payload.set("avatar", StrUtil.trim(avatar));
        log.info("微赞入口参数已生成,tuid={}, third_ts={}", user.getId(), payload.get("third_ts"));
        SymmetricCrypto sm4 = SmUtil.sm4(VZAN_AUTH_SECRET.getBytes(StandardCharsets.UTF_8));
@@ -71,11 +76,11 @@
            throw new IllegalArgumentException("当前用户未绑定诊所");
        }
        Clinic clinic = clinicMapper.selectById(clinicId);
        if (clinic == null || clinic.getAgentId() == null) {
        if (clinic == null) {
            throw new IllegalArgumentException("当前诊所未绑定代理");
        }
        ProvincialAgent agent = provincialAgentMapper.selectById(clinic.getAgentId());
        String contactInfo = agent == null ? null : StrUtil.trim(agent.getContactInfo());
        String contactInfo = StrUtil.trim(clinic.getContactPhone());
        if (StrUtil.isBlank(contactInfo)) {
            throw new IllegalArgumentException("所属代理未配置联系电话");
        }
@@ -95,4 +100,30 @@
            throw new IllegalArgumentException("直播间地址必须为有效的HTTPS地址", ex);
        }
    }
    /**
     * 校验用户存在且可用。
     */
    private AppUser requireEnabledUser(Long userId) {
        AppUser user = userMapper.selectById(userId);
        if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
            throw new IllegalArgumentException("用户不存在");
        }
        if (user.getStatus() == null || user.getStatus().intValue() != USER_STATUS_ENABLED) {
            throw new IllegalArgumentException("用户已禁用");
        }
        return user;
    }
    /**
     * 优先使用本地头像,没有时回退微信头像。
     */
    private String resolveAvatar(AppUser user) {
        String avatar = user == null ? null : StrUtil.trim(user.getAvatar());
        if (StrUtil.isNotBlank(avatar)) {
            return avatar;
        }
        String wechatAvatar = user == null ? null : StrUtil.trim(user.getWechatAvatar());
        return StrUtil.isNotBlank(wechatAvatar) ? wechatAvatar : "";
    }
}
tcm-training-admin/config/routes.ts
@@ -363,6 +363,26 @@
            component: './live/health-popularization/manage',
          },
          {
            name: '项目介绍',
            path: '/live/course/project-introduction',
            access: 'live:project-introduction:view',
            component: './live/project-introduction',
          },
          {
            name: '新增项目介绍',
            path: '/live/course/project-introduction/create',
            hideInMenu: true,
            access: 'live:project-introduction:add',
            component: './live/project-introduction/manage',
          },
          {
            name: '编辑项目介绍',
            path: '/live/course/project-introduction/:id/edit',
            hideInMenu: true,
            access: 'live:project-introduction:edit',
            component: './live/project-introduction/manage',
          },
          {
            name: '视频与课程资源管理',
            path: '/live/course/video-resources',
            access: 'live:health-course:view',
traditionalApp/pages/home/home.vue
@@ -342,7 +342,13 @@
        weiZanLiveOpening.value = true;
        uni.showLoading({ title: '正在进入直播间', mask: true });
        try {
            const res = await createLiveRoomEntry();
            const cachedUser = uni.getStorageSync('appUserInfo');
            const userId = cachedUser?.id;
            if (!userId) {
                uni.showToast({ title: '请先登录', icon: 'none' });
                return;
            }
            const res = await createLiveRoomEntry(userId);
            const targetUrl = res?.data?.url?.trim();
            if (res?.code !== 0 || !targetUrl || !isValidLiveRoomUrl(targetUrl)) {
                uni.showToast({ title: res?.msg || '获取直播间地址失败', icon: 'none' });
traditionalApp/pages/home/service.js
@@ -52,6 +52,6 @@
/**
 * 根据当前登录用户生成短时有效的微赞直播入口。
 */
export const createLiveRoomEntry = () => {
    return request.post('/api/app/home/live-room-entry', {}, false, false)
export const createLiveRoomEntry = (userId) => {
    return request.post('/api/app/home/live-room-entry', { userId }, false, false)
}
traditionalApp/pages/mine/mine.vue
@@ -392,7 +392,12 @@
    if (typeof target !== 'string' && target.dynamicVzan) {
        try {
            uni.showLoading({ title: '正在进入', mask: true })
            const response = await createLiveRoomEntry()
            const userId = userCenter.value.id
            if (!userId) {
                uni.showToast({ title: '请先登录', icon: 'none' })
                return
            }
            const response = await createLiveRoomEntry(userId)
            const targetUrl = response?.code === 0 ? response.data?.url : ''
            if (!targetUrl) {
                uni.showToast({ title: response?.msg || '获取跳转地址失败', icon: 'none' })