소스 검색

益寿缘-记录医生在线离线+优化兔灵接口日志打印

cgp 4 주 전
부모
커밋
bb8ea3cfd3

+ 2 - 0
fs-doctor-app/src/main/java/com/fs/FsDoctorAppApplication.java

@@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
@@ -12,6 +13,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableAsync
+@EnableScheduling
 public class FsDoctorAppApplication
 {
     public static void main(String[] args)

+ 130 - 0
fs-doctor-app/src/main/java/com/fs/app/controller/FsDoctorOnlineController.java

@@ -0,0 +1,130 @@
+package com.fs.app.controller;
+
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.doctor.domain.FsDoctorOnline;
+import com.fs.doctor.service.IFsDoctorOnlineService;
+
+/**
+ * 医生在线状态Controller
+ *
+ * @author fs
+ * @date 2025-12-12
+ */
+@Slf4j
+@RestController
+@RequestMapping("/doctor/online")
+public class FsDoctorOnlineController extends AppBaseController {
+    @Autowired
+    private IFsDoctorOnlineService fsDoctorOnlineService;
+
+    /**
+     * 查询医生在线状态列表
+     */
+    @PreAuthorize("@ss.hasPermi('doctor:online:list')")
+    @GetMapping("/list")
+    public R list(FsDoctorOnline fsDoctorOnline) {
+        PageHelper.startPage(fsDoctorOnline.getPageNum(), fsDoctorOnline.getPageSize());
+        List<FsDoctorOnline> list = fsDoctorOnlineService.selectFsDoctorOnlineList(fsDoctorOnline);
+        PageInfo<FsDoctorOnline> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+
+    /**
+     * 获取医生在线状态详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('doctor:online:query')")
+    @GetMapping(value = "/{doctorId}")
+    public AjaxResult getInfo(@PathVariable("doctorId") Long doctorId) {
+        return AjaxResult.success(fsDoctorOnlineService.selectFsDoctorOnlineByDoctorId(doctorId));
+    }
+
+    /**
+     * 新增医生在线状态
+     */
+    @PreAuthorize("@ss.hasPermi('doctor:online:add')")
+    @Log(title = "医生在线状态", businessType = BusinessType.INSERT)
+    @PostMapping
+    public R add(@RequestBody FsDoctorOnline fsDoctorOnline) {
+        fsDoctorOnlineService.insertFsDoctorOnline(fsDoctorOnline);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 修改医生在线状态
+     */
+    @PreAuthorize("@ss.hasPermi('doctor:online:edit')")
+    @Log(title = "医生在线状态", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public R edit(@RequestBody FsDoctorOnline fsDoctorOnline) {
+        fsDoctorOnlineService.updateFsDoctorOnline(fsDoctorOnline);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 删除医生在线状态
+     */
+    @PreAuthorize("@ss.hasPermi('doctor:online:remove')")
+    @Log(title = "医生在线状态", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{doctorIds}")
+    public R remove(@PathVariable Long[] doctorIds) {
+        fsDoctorOnlineService.deleteFsDoctorOnlineByDoctorIds(doctorIds);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 心跳接口 - 前端定期调用,更新在线状态
+     */
+    @PostMapping("/heartbeat")
+    public AjaxResult heartbeat() {
+        try {
+            if (StringUtils.isBlank(getDoctorId())) {
+                return AjaxResult.error("未获取到医生ID");
+            }
+            Long doctorId = Long.parseLong(getDoctorId());
+            fsDoctorOnlineService.heartbeat(doctorId);
+            return AjaxResult.success();
+        } catch (CustomException e) {
+            log.error("心跳异常, doctorId: {}", getDoctorId(), e);
+            return AjaxResult.error("心跳机制失败,请稍后重试");
+        }
+    }
+
+    /**
+     * 主动登出 - 标记为离线(用于正常退出时)
+     */
+    @PostMapping("/updateOnlineStatusOnLogout")
+    public AjaxResult updateOnlineStatusOnLogout() {
+        try {
+            if (StringUtils.isBlank(getDoctorId())) {
+                return AjaxResult.error("未获取到医生ID");
+            }
+            Long doctorId = Long.parseLong(getDoctorId());
+            fsDoctorOnlineService.updateOnlineStatusOnLogout(doctorId);
+            return AjaxResult.success();
+        } catch (CustomException e) {
+            log.error("登出登录更新在线状态异常, doctorId: {}", getDoctorId(), e);
+            return AjaxResult.error("登出登录更新在线状态异常");
+        }
+    }
+}

+ 30 - 0
fs-doctor-app/src/main/java/com/fs/framework/manager/DoctorOnlineStatusScheduler.java

@@ -0,0 +1,30 @@
+package com.fs.framework.manager;
+
+import com.fs.doctor.service.IFsDoctorOnlineService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+@Slf4j
+@Component
+public class DoctorOnlineStatusScheduler {
+
+    @Autowired
+    private IFsDoctorOnlineService doctorOnlineService;
+
+    // 每 60 秒执行一次
+    @Scheduled(fixedRate = 60_000)
+    public void updateOfflineDoctors() {
+        //log.warn("医生在线状态兜底任务执行中...");
+        //如果某个医生最后心跳时间早于当前时间 90 秒前,就认为他已离线。
+        LocalDateTime timeout = LocalDateTime.now().minusSeconds(90);
+        
+        int updated = doctorOnlineService.setOfflineByTimeout(timeout);
+        
+        if (updated > 0) {
+            log.info("自动下线 {} 名医生", updated);
+        }
+    }
+}

+ 31 - 0
fs-service/src/main/java/com/fs/doctor/domain/FsDoctorOnline.java

@@ -0,0 +1,31 @@
+package com.fs.doctor.domain;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.his.param.BaseParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 医生在线状态对象 fs_doctor_online_info
+ *
+ * @author fs
+ * @date 2025-12-12
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsDoctorOnline  extends BaseParam implements Serializable{
+
+    /** 关联 fs_doctor.doctor_id */
+    private Long doctorId;
+
+    /** 最后心跳时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime lastHeartbeat;
+
+    /** 是否在线(0否 1是) */
+    private Integer isOnline;
+
+
+}

+ 78 - 0
fs-service/src/main/java/com/fs/doctor/mapper/FsDoctorOnlineMapper.java

@@ -0,0 +1,78 @@
+package com.fs.doctor.mapper;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.doctor.domain.FsDoctorOnline;
+
+/**
+ * 医生在线状态Mapper接口
+ * 
+ * @author fs
+ * @date 2025-12-12
+ */
+public interface FsDoctorOnlineMapper extends BaseMapper<FsDoctorOnline>{
+    /**
+     * 查询医生在线状态
+     * 
+     * @param doctorId 医生在线状态主键
+     * @return 医生在线状态
+     */
+    FsDoctorOnline selectFsDoctorOnlineByDoctorId(Long doctorId);
+
+    /**
+     * 查询医生在线状态列表
+     * 
+     * @param fsDoctorOnline 医生在线状态
+     * @return 医生在线状态集合
+     */
+    List<FsDoctorOnline> selectFsDoctorOnlineList(FsDoctorOnline fsDoctorOnline);
+
+    /**
+     * 新增医生在线状态
+     * 
+     * @param fsDoctorOnline 医生在线状态
+     * @return 结果
+     */
+    int insertFsDoctorOnline(FsDoctorOnline fsDoctorOnline);
+
+    /**
+     * 新增医生在线心跳机制 原子操作,避免并发插入冲突
+     *
+     * @param fsDoctorOnline 医生在线状态
+     * @return 结果
+     */
+    int upsertHeartbeat(FsDoctorOnline fsDoctorOnline);
+
+    /**
+     * 修改医生在线状态
+     * 
+     * @param fsDoctorOnline 医生在线状态
+     * @return 结果
+     */
+    int updateFsDoctorOnline(FsDoctorOnline fsDoctorOnline);
+
+    /**
+     * 删除医生在线状态
+     * 
+     * @param doctorId 医生在线状态主键
+     * @return 结果
+     */
+    int deleteFsDoctorOnlineByDoctorId(Long doctorId);
+
+    /**
+     * 批量删除医生在线状态
+     * 
+     * @param doctorIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsDoctorOnlineByDoctorIds(Long[] doctorIds);
+
+    /**
+     * 批量设置离线
+     *
+     * @param timeout
+     * @return
+     */
+    int setOfflineByTimeout(LocalDateTime timeout);
+}

+ 83 - 0
fs-service/src/main/java/com/fs/doctor/service/IFsDoctorOnlineService.java

@@ -0,0 +1,83 @@
+package com.fs.doctor.service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.doctor.domain.FsDoctorOnline;
+
+/**
+ * 医生在线状态Service接口
+ * 
+ * @author fs
+ * @date 2025-12-12
+ */
+public interface IFsDoctorOnlineService extends IService<FsDoctorOnline>{
+    /**
+     * 查询医生在线状态
+     * 
+     * @param doctorId 医生在线状态主键
+     * @return 医生在线状态
+     */
+    FsDoctorOnline selectFsDoctorOnlineByDoctorId(Long doctorId);
+
+    /**
+     * 查询医生在线状态列表
+     * 
+     * @param fsDoctorOnline 医生在线状态
+     * @return 医生在线状态集合
+     */
+    List<FsDoctorOnline> selectFsDoctorOnlineList(FsDoctorOnline fsDoctorOnline);
+
+    /**
+     * 新增医生在线状态
+     * 
+     * @param fsDoctorOnline 医生在线状态
+     * @return 结果
+     */
+    int insertFsDoctorOnline(FsDoctorOnline fsDoctorOnline);
+
+    /**
+     * 修改医生在线状态
+     * 
+     * @param fsDoctorOnline 医生在线状态
+     * @return 结果
+     */
+    int updateFsDoctorOnline(FsDoctorOnline fsDoctorOnline);
+
+    /**
+     * 批量删除医生在线状态
+     * 
+     * @param doctorIds 需要删除的医生在线状态主键集合
+     * @return 结果
+     */
+    int deleteFsDoctorOnlineByDoctorIds(Long[] doctorIds);
+
+    /**
+     * 删除医生在线状态信息
+     * 
+     * @param doctorId 医生在线状态主键
+     * @return 结果
+     */
+    int deleteFsDoctorOnlineByDoctorId(Long doctorId);
+
+    /**
+     * 心跳接口 - 前端定期调用,更新在线状态
+     *
+     * @param doctorId
+     */
+    int heartbeat(Long doctorId);
+
+    /**
+     * 主动登出 - 标记为离线(用于正常退出时)
+     *
+     * @param doctorId
+     */
+    int updateOnlineStatusOnLogout(Long doctorId);
+
+    /**
+     * 定时检查 - 离线医生处理
+     *
+     * @param timeout
+     */
+    int setOfflineByTimeout(LocalDateTime timeout);
+}

+ 113 - 0
fs-service/src/main/java/com/fs/doctor/service/impl/FsDoctorOnlineServiceImpl.java

@@ -0,0 +1,113 @@
+package com.fs.doctor.service.impl;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.doctor.mapper.FsDoctorOnlineMapper;
+import com.fs.doctor.domain.FsDoctorOnline;
+import com.fs.doctor.service.IFsDoctorOnlineService;
+
+/**
+ * 医生在线状态Service业务层处理
+ *
+ * @author fs
+ * @date 2025-12-12
+ */
+@Service
+public class FsDoctorOnlineServiceImpl extends ServiceImpl<FsDoctorOnlineMapper, FsDoctorOnline> implements IFsDoctorOnlineService {
+
+    @Autowired
+    private FsDoctorOnlineMapper fsDoctorOnlineMapper;
+
+    /**
+     * 查询医生在线状态
+     *
+     * @param doctorId 医生在线状态主键
+     * @return 医生在线状态
+     */
+    @Override
+    public FsDoctorOnline selectFsDoctorOnlineByDoctorId(Long doctorId) {
+        return fsDoctorOnlineMapper.selectFsDoctorOnlineByDoctorId(doctorId);
+    }
+
+    /**
+     * 查询医生在线状态列表
+     *
+     * @param fsDoctorOnline 医生在线状态
+     * @return 医生在线状态
+     */
+    @Override
+    public List<FsDoctorOnline> selectFsDoctorOnlineList(FsDoctorOnline fsDoctorOnline) {
+        return fsDoctorOnlineMapper.selectFsDoctorOnlineList(fsDoctorOnline);
+    }
+
+    /**
+     * 新增医生在线状态
+     *
+     * @param fsDoctorOnline 医生在线状态
+     * @return 结果
+     */
+    @Override
+    public int insertFsDoctorOnline(FsDoctorOnline fsDoctorOnline) {
+        return fsDoctorOnlineMapper.insertFsDoctorOnline(fsDoctorOnline);
+    }
+
+    /**
+     * 修改医生在线状态
+     *
+     * @param fsDoctorOnline 医生在线状态
+     * @return 结果
+     */
+    @Override
+    public int updateFsDoctorOnline(FsDoctorOnline fsDoctorOnline) {
+        return fsDoctorOnlineMapper.updateFsDoctorOnline(fsDoctorOnline);
+    }
+
+    /**
+     * 批量删除医生在线状态
+     *
+     * @param doctorIds 需要删除的医生在线状态主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsDoctorOnlineByDoctorIds(Long[] doctorIds) {
+        return fsDoctorOnlineMapper.deleteFsDoctorOnlineByDoctorIds(doctorIds);
+    }
+
+    /**
+     * 删除医生在线状态信息
+     *
+     * @param doctorId 医生在线状态主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsDoctorOnlineByDoctorId(Long doctorId) {
+        return fsDoctorOnlineMapper.deleteFsDoctorOnlineByDoctorId(doctorId);
+    }
+
+    @Override
+    public int heartbeat(Long doctorId) {
+        FsDoctorOnline record = new FsDoctorOnline();
+        record.setDoctorId(doctorId);
+        record.setLastHeartbeat(LocalDateTime.now());
+        record.setIsOnline(1);
+        return fsDoctorOnlineMapper.upsertHeartbeat(record);
+    }
+
+    @Override
+    public int updateOnlineStatusOnLogout(Long doctorId) {
+        FsDoctorOnline record = new FsDoctorOnline();
+        record.setDoctorId(doctorId);
+        record.setLastHeartbeat(LocalDateTime.now()); // 正常登出,更新为当前时间
+        record.setIsOnline(0);
+        return fsDoctorOnlineMapper.upsertHeartbeat(record);
+    }
+
+    @Override
+    public int setOfflineByTimeout(LocalDateTime timeout) {
+        return fsDoctorOnlineMapper.setOfflineByTimeout(timeout);
+    }
+}

+ 13 - 3
fs-service/src/main/java/com/fs/erp/service/impl/TlErpOrderServiceImpl.java

@@ -69,12 +69,22 @@ public class TlErpOrderServiceImpl implements TlErpOrderService {
             HttpResponse response = cn.hutool.http.HttpRequest.post(url)
                     .header("Content-Type", "application/json;charset=UTF-8")
                     .body(requestBody)
-                    .timeout(10000) // 可选:设置超时(毫秒)
+                    .timeout(10000)
                     .execute();
 
             if (!response.isOk()) {
-                log.error("HTTP 请求失败,状态码: {}", response.getStatus());
-                throw new RuntimeException("调用[兔灵]接口失败,HTTP 状态码: " + response.getStatus());
+                String responseBody = response.body();
+                Map<String, List<String>> headers = response.headers(); // 获取所有响应头
+                int statusCode = response.getStatus();
+
+                log.error(
+                        "HTTP 请求失败 - 状态码: {}, 响应头: {}, 响应体: {}",
+                        statusCode,
+                        headers,
+                        responseBody != null ? responseBody : "无响应体"
+                );
+
+                throw new RuntimeException("调用[兔灵]接口失败,状态码: " + statusCode + ", 详情见日志");
             }
 
             // 解析响应体

+ 8 - 0
fs-service/src/main/java/com/fs/wxwork/utils/WxWorkHttpUtil.java

@@ -298,4 +298,12 @@ public class WxWorkHttpUtil {
             logger.info("响应体: {}", logBody);
         }
     }
+
+    /**
+     * 发送GET请求并自动反序列化为指定泛型类型
+     */
+    public static <T> T getWithType(String url, TypeReference<T> typeReference) {
+        String responseBody = get(url); // 复用带日志的 get 方法
+        return JSON.parseObject(responseBody, typeReference);
+    }
 }

+ 78 - 0
fs-service/src/main/resources/mapper/doctor/FsDoctorOnlineMapper.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.doctor.mapper.FsDoctorOnlineMapper">
+
+    <resultMap type="FsDoctorOnline" id="FsDoctorOnlineResult">
+        <result property="doctorId"    column="doctor_id"    />
+        <result property="lastHeartbeat"    column="last_heartbeat"    />
+        <result property="isOnline"    column="is_online"    />
+    </resultMap>
+
+    <sql id="selectFsDoctorOnlineVo">
+        select doctor_id, last_heartbeat, is_online from fs_doctor_online_info
+    </sql>
+
+    <select id="selectFsDoctorOnlineList" parameterType="FsDoctorOnline" resultMap="FsDoctorOnlineResult">
+        <include refid="selectFsDoctorOnlineVo"/>
+        <where>
+            <if test="lastHeartbeat != null "> and last_heartbeat = #{lastHeartbeat}</if>
+            <if test="isOnline != null "> and is_online = #{isOnline}</if>
+        </where>
+    </select>
+
+    <select id="selectFsDoctorOnlineByDoctorId" parameterType="Long" resultMap="FsDoctorOnlineResult">
+        <include refid="selectFsDoctorOnlineVo"/>
+        where doctor_id = #{doctorId}
+    </select>
+
+    <insert id="insertFsDoctorOnline" parameterType="FsDoctorOnline">
+        insert into fs_doctor_online_info
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="doctorId != null">doctor_id,</if>
+            <if test="lastHeartbeat != null">last_heartbeat,</if>
+            <if test="isOnline != null">is_online,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="doctorId != null">#{doctorId},</if>
+            <if test="lastHeartbeat != null">#{lastHeartbeat},</if>
+            <if test="isOnline != null">#{isOnline},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsDoctorOnline" parameterType="FsDoctorOnline">
+        update fs_doctor_online_info
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="lastHeartbeat != null">last_heartbeat = #{lastHeartbeat},</if>
+            <if test="isOnline != null">is_online = #{isOnline},</if>
+        </trim>
+        where doctor_id = #{doctorId}
+    </update>
+
+    <update id="setOfflineByTimeout">
+        UPDATE fs_doctor_online_info
+        SET is_online = 0
+        WHERE is_online = 1
+          AND last_heartbeat &lt; #{timeout}
+    </update>
+
+    <delete id="deleteFsDoctorOnlineByDoctorId" parameterType="Long">
+        delete from fs_doctor_online_info where doctor_id = #{doctorId}
+    </delete>
+
+    <delete id="deleteFsDoctorOnlineByDoctorIds" parameterType="String">
+        delete from fs_doctor_online_info where doctor_id in
+        <foreach item="doctorId" collection="array" open="(" separator="," close=")">
+            #{doctorId}
+        </foreach>
+    </delete>
+
+    <insert id="upsertHeartbeat">
+        INSERT INTO fs_doctor_online_info (doctor_id, last_heartbeat, is_online)
+        VALUES (#{doctorId}, #{lastHeartbeat}, #{isOnline})
+        ON DUPLICATE KEY UPDATE
+                             last_heartbeat = VALUES(last_heartbeat),
+                             is_online = VALUES(is_online)
+    </insert>
+</mapper>