update plugins/freeswitch: add 3 mod 18 del 3 files

This commit is contained in:
jack ning
2025-06-08 17:46:51 +08:00
parent a48f845496
commit cdfc6dc5a4
22 changed files with 1270 additions and 894 deletions

View File

@@ -23,7 +23,7 @@ import com.bytedesk.freeswitch.callcenter.event.CallAnsweredEvent;
import com.bytedesk.freeswitch.callcenter.event.CallHangupEvent;
import com.bytedesk.freeswitch.callcenter.event.CallStartEvent;
import com.bytedesk.freeswitch.callcenter.event.DtmfEvent;
import com.bytedesk.freeswitch.service.FreeSwitchUserService;
import com.bytedesk.freeswitch.user.FreeSwitchUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -25,10 +25,10 @@ import org.springframework.stereotype.Service;
// import com.bytedesk.freeswitch.freeswitch.FreeSwitchProperties;
import com.bytedesk.freeswitch.freeswitch.FreeSwitchService;
import com.bytedesk.freeswitch.model.FreeSwitchCdrEntity;
import com.bytedesk.freeswitch.model.FreeSwitchUserEntity;
import com.bytedesk.freeswitch.service.FreeSwitchCdrService;
import com.bytedesk.freeswitch.service.FreeSwitchUserService;
import com.bytedesk.freeswitch.cdr.FreeSwitchCdrEntity;
import com.bytedesk.freeswitch.user.FreeSwitchUserEntity;
import com.bytedesk.freeswitch.cdr.FreeSwitchCdrService;
import com.bytedesk.freeswitch.user.FreeSwitchUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -1,320 +0,0 @@
package com.bytedesk.freeswitch.controller;
import com.bytedesk.core.rbac.annotation.CurrentUser;
import com.bytedesk.core.rbac.user.UserEntity;
import com.bytedesk.core.utils.JsonResult;
import com.bytedesk.freeswitch.dto.FreeSwitchCdrDto;
import com.bytedesk.freeswitch.dto.FreeSwitchDtoMapper;
import com.bytedesk.freeswitch.model.FreeSwitchCdrEntity;
import com.bytedesk.freeswitch.service.FreeSwitchCdrService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* FreeSwitch通话详单REST API控制器
*/
@Slf4j
@RestController
@RequestMapping("/freeswitch/api/v1/cdr")
@AllArgsConstructor
@ConditionalOnProperty(name = "bytedesk.freeswitch.enabled", havingValue = "true")
public class FreeSwitchCdrController {
private final FreeSwitchCdrService cdrService;
private final FreeSwitchDtoMapper dtoMapper;
/**
* 获取通话详单列表
*/
@GetMapping
public ResponseEntity<JsonResult<Page<FreeSwitchCdrDto>>> getCdrList(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "startTime") String sort,
@RequestParam(defaultValue = "desc") String direction,
@RequestParam(required = false) String callerNumber,
@RequestParam(required = false) String destinationNumber,
@RequestParam(required = false) String direction_filter,
@RequestParam(required = false) String hangupCause,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
try {
Sort.Direction sortDirection = "asc".equalsIgnoreCase(direction) ?
Sort.Direction.ASC : Sort.Direction.DESC;
Pageable pageable = PageRequest.of(page, size, Sort.by(sortDirection, sort));
Page<FreeSwitchCdrEntity> entities;
if (startTime != null && endTime != null) {
entities = cdrService.findByTimeRange(startTime, endTime, pageable);
} else if (callerNumber != null) {
entities = cdrService.findByCallerNumber(callerNumber, pageable);
} else if (destinationNumber != null) {
entities = cdrService.findByDestinationNumber(destinationNumber, pageable);
} else if (direction_filter != null) {
entities = cdrService.findByDirection(direction_filter, pageable);
} else if (hangupCause != null) {
entities = cdrService.findByHangupCause(hangupCause, pageable);
} else {
entities = cdrService.findAll(pageable);
}
Page<FreeSwitchCdrDto> dtos = entities.map(dtoMapper::toCdrDto);
return ResponseEntity.ok(JsonResult.success(dtos));
} catch (Exception e) {
log.error("获取FreeSwitch CDR列表失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 根据ID获取通话详单详情
*/
@GetMapping("/{id}")
public ResponseEntity<JsonResult<FreeSwitchCdrDto>> getCdrById(@PathVariable Long id) {
try {
Optional<FreeSwitchCdrEntity> entity = cdrService.findById(id);
if (entity.isPresent()) {
FreeSwitchCdrDto dto = dtoMapper.toCdrDto(entity.get());
return ResponseEntity.ok(JsonResult.success(dto));
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
log.error("获取FreeSwitch CDR详情失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 根据UUID获取通话详单详情
*/
@GetMapping("/uuid/{uuid}")
public ResponseEntity<JsonResult<FreeSwitchCdrDto>> getCdrByUuid(@PathVariable String uuid) {
try {
Optional<FreeSwitchCdrEntity> entity = cdrService.findByUuid(uuid);
if (entity.isPresent()) {
FreeSwitchCdrDto dto = dtoMapper.toCdrDto(entity.get());
return ResponseEntity.ok(JsonResult.success(dto));
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
log.error("获取FreeSwitch CDR详情失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取今日通话统计
*/
@GetMapping("/statistics/today")
public ResponseEntity<JsonResult<FreeSwitchCdrService.CallStatistics>> getTodayStatistics() {
try {
FreeSwitchCdrService.CallStatistics statistics = cdrService.getTodayStatistics();
return ResponseEntity.ok(JsonResult.success(statistics));
} catch (Exception e) {
log.error("获取今日通话统计失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取时间范围内的通话统计
*/
@GetMapping("/statistics/range")
public ResponseEntity<JsonResult<FreeSwitchCdrService.CallStatistics>> getStatisticsByRange(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
try {
FreeSwitchCdrService.CallStatistics statistics = cdrService.getStatisticsByTimeRange(startTime, endTime);
return ResponseEntity.ok(JsonResult.success(statistics));
} catch (Exception e) {
log.error("获取时间范围通话统计失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取用户通话统计
*/
@GetMapping("/statistics/user/{callerNumber}")
public ResponseEntity<JsonResult<FreeSwitchCdrService.CallStatistics>> getUserStatistics(
@PathVariable String callerNumber,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
try {
FreeSwitchCdrService.CallStatistics statistics;
if (startTime != null && endTime != null) {
statistics = cdrService.getUserStatistics(callerNumber, startTime, endTime);
} else {
statistics = cdrService.getUserStatistics(callerNumber);
}
return ResponseEntity.ok(JsonResult.success(statistics));
} catch (Exception e) {
log.error("获取用户通话统计失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取通话方向统计
*/
@GetMapping("/statistics/direction")
public ResponseEntity<JsonResult<List<Object[]>>> getCallDirectionStatistics(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
try {
List<Object[]> statistics;
if (startTime != null && endTime != null) {
statistics = cdrService.getCallDirectionStatistics(startTime, endTime);
} else {
statistics = cdrService.getCallDirectionStatistics();
}
return ResponseEntity.ok(JsonResult.success(statistics));
} catch (Exception e) {
log.error("获取通话方向统计失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取挂断原因统计
*/
@GetMapping("/statistics/hangup-cause")
public ResponseEntity<JsonResult<List<Object[]>>> getHangupCauseStatistics(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
try {
List<Object[]> statistics;
if (startTime != null && endTime != null) {
statistics = cdrService.getHangupCauseStatistics(startTime, endTime);
} else {
statistics = cdrService.getHangupCauseStatistics();
}
return ResponseEntity.ok(JsonResult.success(statistics));
} catch (Exception e) {
log.error("获取挂断原因统计失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取长通话记录
*/
@GetMapping("/long-calls")
public ResponseEntity<JsonResult<List<FreeSwitchCdrDto>>> getLongCalls(
@RequestParam(defaultValue = "300") long minDuration) {
try {
List<FreeSwitchCdrEntity> entities = cdrService.findLongCalls(minDuration);
List<FreeSwitchCdrDto> dtos = entities.stream()
.map(dtoMapper::toCdrDto)
.collect(Collectors.toList());
return ResponseEntity.ok(JsonResult.success(dtos));
} catch (Exception e) {
log.error("获取长通话记录失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取失败通话记录
*/
@GetMapping("/failed-calls")
public ResponseEntity<JsonResult<Page<FreeSwitchCdrDto>>> getFailedCalls(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
try {
Pageable pageable = PageRequest.of(page, size, Sort.by("startTime").descending());
Page<FreeSwitchCdrEntity> entities = cdrService.findFailedCalls(pageable);
Page<FreeSwitchCdrDto> dtos = entities.map(dtoMapper::toCdrDto);
return ResponseEntity.ok(JsonResult.success(dtos));
} catch (Exception e) {
log.error("获取失败通话记录失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 删除过期的CDR记录
*/
@DeleteMapping("/cleanup")
public ResponseEntity<JsonResult<Integer>> cleanupExpiredCdr(
@RequestParam(defaultValue = "90") int retentionDays,
@CurrentUser UserEntity currentUser) {
try {
int deletedCount = cdrService.cleanupExpiredRecords(retentionDays);
return ResponseEntity.ok(JsonResult.success(deletedCount,
String.format("已清理%d条过期CDR记录", deletedCount)));
} catch (Exception e) {
log.error("清理过期CDR记录失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 搜索通话记录
*/
@GetMapping("/search")
public ResponseEntity<JsonResult<Page<FreeSwitchCdrDto>>> searchCdr(
@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
try {
Pageable pageable = PageRequest.of(page, size, Sort.by("startTime").descending());
Page<FreeSwitchCdrEntity> entities = cdrService.searchCdr(keyword, pageable);
Page<FreeSwitchCdrDto> dtos = entities.map(dtoMapper::toCdrDto);
return ResponseEntity.ok(JsonResult.success(dtos));
} catch (Exception e) {
log.error("搜索CDR记录失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 导出CDR记录
*/
@GetMapping("/export")
public ResponseEntity<JsonResult<String>> exportCdr(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime,
@RequestParam(defaultValue = "csv") String format) {
try {
// 这里可以实现实际的导出逻辑
// 返回导出文件的下载链接或直接返回数据
String message = String.format("CDR记录导出请求已提交格式: %s", format);
if (startTime != null && endTime != null) {
message += String.format(",时间范围: %s 至 %s", startTime, endTime);
}
return ResponseEntity.ok(JsonResult.success("", message));
} catch (Exception e) {
log.error("导出CDR记录失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
}

View File

@@ -1,79 +0,0 @@
package com.bytedesk.freeswitch.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* FreeSwitch通话详单DTO
*/
@Data
public class FreeSwitchCdrDto {
private Long id;
private String uuid;
private String callerIdName;
private String callerIdNumber;
private String destinationNumber;
private String context;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime answerTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
private Long duration;
private Long billDuration;
private String hangupCause;
private String accountCode;
private String direction;
private String readCodec;
private String writeCodec;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
// 计算属性
public String getFormattedDuration() {
if (duration == null) return "00:00:00";
long hours = duration / 3600;
long minutes = (duration % 3600) / 60;
long seconds = duration % 60;
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
public String getCallStatus() {
if (answerTime == null) {
return "未接听";
} else if ("NORMAL_CLEARING".equals(hangupCause)) {
return "正常结束";
} else if ("USER_BUSY".equals(hangupCause)) {
return "用户忙";
} else if ("NO_ANSWER".equals(hangupCause)) {
return "无应答";
} else {
return "其他";
}
}
}

View File

@@ -11,7 +11,7 @@
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.model;
package com.bytedesk.freeswitch.cdr;
import java.time.LocalDateTime;

View File

@@ -11,7 +11,7 @@
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.model;
package com.bytedesk.freeswitch.cdr;
import org.springframework.stereotype.Component;

View File

@@ -11,7 +11,7 @@
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.repository;
package com.bytedesk.freeswitch.cdr;
import java.time.LocalDateTime;
import java.util.List;
@@ -25,8 +25,6 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.bytedesk.freeswitch.model.FreeSwitchCdrEntity;
/**
* FreeSwitch CDR仓库接口
*/

View File

@@ -2,7 +2,7 @@
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-09 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-09 10:00:00
* @LastEditTime: 2025-06-08 17:40:37
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
@@ -11,7 +11,7 @@
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.service;
package com.bytedesk.freeswitch.cdr;
import java.time.LocalDateTime;
import java.util.List;
@@ -23,9 +23,6 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bytedesk.freeswitch.model.FreeSwitchCdrEntity;
import com.bytedesk.freeswitch.repository.FreeSwitchCdrRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -1,32 +0,0 @@
package com.bytedesk.freeswitch.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import javax.validation.constraints.Min;
/**
* 创建FreeSwitch会议室请求DTO
*/
@Data
public class CreateFreeSwitchConferenceRequest {
@NotBlank(message = "会议室名称不能为空")
@Size(min = 2, max = 50, message = "会议室名称长度必须在2-50个字符之间")
private String name;
@Size(max = 200, message = "描述不能超过200个字符")
private String description;
@Size(min = 4, max = 20, message = "主持人PIN长度必须在4-20个字符之间")
private String moderatorPin;
@Size(min = 4, max = 20, message = "成员PIN长度必须在4-20个字符之间")
private String memberPin;
@Min(value = 1, message = "最大成员数必须大于0")
private Integer maxMembers;
private Boolean active = true;
}

View File

@@ -1,3 +1,16 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-08 16:01:30
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-08 17:26:54
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.dto;
import com.fasterxml.jackson.annotation.JsonFormat;

View File

@@ -79,4 +79,11 @@ public class FreeSwitchConferenceRequest extends BaseRequest {
* 备注
*/
private String remarks;
/**
* 获取主持人密码(兼容性方法)
*/
public String getModeratorPin() {
return this.password;
}
}

View File

@@ -17,14 +17,14 @@ import com.bytedesk.core.base.BaseResponse;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
@Data
@Builder
@SuperBuilder
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@@ -86,4 +86,26 @@ public class FreeSwitchConferenceResponse extends BaseResponse {
* 是否有密码保护
*/
private Boolean passwordProtected;
/**
* 从实体对象创建响应对象
*/
public static FreeSwitchConferenceResponse fromEntity(FreeSwitchConferenceEntity entity) {
return FreeSwitchConferenceResponse.builder()
.uid(entity.getUid())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.conferenceName(entity.getConferenceName())
.description(entity.getDescription())
.password(entity.getPassword())
.maxMembers(entity.getMaxMembers())
.enabled(entity.getEnabled())
.recordEnabled(entity.getRecordEnabled())
.recordPath(entity.getRecordPath())
.creator(entity.getCreator())
.configJson(entity.getConfigJson())
.remarks(entity.getRemarks())
.passwordProtected(entity.getPassword() != null && !entity.getPassword().isEmpty())
.build();
}
}

View File

@@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* FreeSwitch会议服务类
@@ -63,6 +64,20 @@ public class FreeSwitchConferenceService {
return conferenceRepository.findById(id);
}
/**
* 分页查询所有会议室
*/
public Page<FreeSwitchConferenceEntity> findAll(Pageable pageable) {
return conferenceRepository.findAll(pageable);
}
/**
* 根据会议室名称模糊查询
*/
public Page<FreeSwitchConferenceEntity> findByConferenceNameContaining(String keyword, Pageable pageable) {
return conferenceRepository.findByConferenceNameContainingIgnoreCase(keyword, pageable);
}
/**
* 根据会议室名称查找会议室
*/
@@ -157,6 +172,51 @@ public class FreeSwitchConferenceService {
return false;
}
/**
* 根据ID删除会议室
*/
@Transactional
public void deleteById(Long id) {
conferenceRepository.deleteById(id);
log.info("删除会议室: ID = {}", id);
}
/**
* 切换会议室状态
*/
@Transactional
public void toggleStatus(Long id) {
Optional<FreeSwitchConferenceEntity> conferenceOpt = conferenceRepository.findById(id);
if (conferenceOpt.isPresent()) {
FreeSwitchConferenceEntity conference = conferenceOpt.get();
conference.setEnabled(!conference.getEnabled());
conferenceRepository.save(conference);
log.info("切换会议室状态: {} -> {}", conference.getConferenceName(), conference.getEnabled() ? "启用" : "禁用");
} else {
throw new RuntimeException("会议室不存在: " + id);
}
}
/**
* 根据启用状态查找会议室
*/
public List<FreeSwitchConferenceEntity> findByEnabled(Boolean enabled) {
if (enabled) {
return conferenceRepository.findByEnabledTrue();
} else {
return conferenceRepository.findByEnabledFalse();
}
}
/**
* 查找最大成员数量大于等于指定值的会议室
*/
public List<FreeSwitchConferenceEntity> findByMaxMembersGreaterThanEqual(Integer minCapacity) {
return conferenceRepository.findAll().stream()
.filter(conference -> conference.getMaxMembers() != null && conference.getMaxMembers() >= minCapacity)
.collect(Collectors.toList());
}
/**
* 获取所有会议室(分页)
*/

View File

@@ -1,110 +0,0 @@
package com.bytedesk.freeswitch.dto;
import com.bytedesk.freeswitch.model.FreeSwitchUserEntity;
import com.bytedesk.freeswitch.model.FreeSwitchCdrEntity;
import com.bytedesk.freeswitch.model.FreeSwitchConferenceEntity;
import com.bytedesk.freeswitch.model.FreeSwitchGatewayEntity;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
/**
* FreeSwitch DTO转换工具类
*/
@Component
public class FreeSwitchDtoMapper {
/**
* 用户实体转DTO
*/
public FreeSwitchUserDto toUserDto(FreeSwitchUserEntity entity) {
if (entity == null) {
return null;
}
FreeSwitchUserDto dto = new FreeSwitchUserDto();
BeanUtils.copyProperties(entity, dto);
return dto;
}
/**
* CDR实体转DTO
*/
public FreeSwitchCdrDto toCdrDto(FreeSwitchCdrEntity entity) {
if (entity == null) {
return null;
}
FreeSwitchCdrDto dto = new FreeSwitchCdrDto();
BeanUtils.copyProperties(entity, dto);
return dto;
}
/**
* 会议室实体转DTO
*/
public FreeSwitchConferenceDto toConferenceDto(FreeSwitchConferenceEntity entity) {
if (entity == null) {
return null;
}
FreeSwitchConferenceDto dto = new FreeSwitchConferenceDto();
BeanUtils.copyProperties(entity, dto);
return dto;
}
/**
* 网关实体转DTO
*/
public FreeSwitchGatewayDto toGatewayDto(FreeSwitchGatewayEntity entity) {
if (entity == null) {
return null;
}
FreeSwitchGatewayDto dto = new FreeSwitchGatewayDto();
BeanUtils.copyProperties(entity, dto);
// 不复制密码字段
dto.setPassword(null);
return dto;
}
/**
* 创建用户请求转实体
*/
public FreeSwitchUserEntity toUserEntity(CreateFreeSwitchUserRequest request) {
if (request == null) {
return null;
}
FreeSwitchUserEntity entity = new FreeSwitchUserEntity();
BeanUtils.copyProperties(request, entity);
return entity;
}
/**
* 创建会议室请求转实体
*/
public FreeSwitchConferenceEntity toConferenceEntity(CreateFreeSwitchConferenceRequest request) {
if (request == null) {
return null;
}
FreeSwitchConferenceEntity entity = new FreeSwitchConferenceEntity();
BeanUtils.copyProperties(request, entity);
entity.setCurrentMembers(0); // 初始成员数为0
return entity;
}
/**
* 创建网关请求转实体
*/
public FreeSwitchGatewayEntity toGatewayEntity(CreateFreeSwitchGatewayRequest request) {
if (request == null) {
return null;
}
FreeSwitchGatewayEntity entity = new FreeSwitchGatewayEntity();
BeanUtils.copyProperties(request, entity);
entity.setStatus("CREATED"); // 初始状态
return entity;
}
}

View File

@@ -13,10 +13,10 @@ import com.bytedesk.freeswitch.callcenter.event.CallAnsweredEvent;
import com.bytedesk.freeswitch.callcenter.event.CallHangupEvent;
import com.bytedesk.freeswitch.callcenter.event.CallStartEvent;
import com.bytedesk.freeswitch.callcenter.event.DtmfEvent;
import com.bytedesk.freeswitch.model.FreeSwitchCdrEntity;
import com.bytedesk.freeswitch.model.FreeSwitchUserEntity;
import com.bytedesk.freeswitch.service.FreeSwitchCdrService;
import com.bytedesk.freeswitch.service.FreeSwitchUserService;
import com.bytedesk.freeswitch.cdr.FreeSwitchCdrEntity;
import com.bytedesk.freeswitch.user.FreeSwitchUserEntity;
import com.bytedesk.freeswitch.cdr.FreeSwitchCdrService;
import com.bytedesk.freeswitch.user.FreeSwitchUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -0,0 +1,107 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-09 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-09 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.gateway;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* FreeSwitch网关请求实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FreeSwitchGatewayRequest {
/**
* 网关名称
*/
@NotBlank(message = "网关名称不能为空")
@Size(max = 100, message = "网关名称长度不能超过100字符")
private String gatewayName;
/**
* 网关描述
*/
@Size(max = 255, message = "网关描述长度不能超过255字符")
private String description;
/**
* SIP服务器地址
*/
@NotBlank(message = "SIP服务器地址不能为空")
@Size(max = 255, message = "SIP服务器地址长度不能超过255字符")
private String proxy;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
@Size(max = 100, message = "用户名长度不能超过100字符")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Size(max = 255, message = "密码长度不能超过255字符")
private String password;
/**
* 从号码
*/
@Size(max = 100, message = "从号码长度不能超过100字符")
private String fromUser;
/**
* 从域名
*/
@Size(max = 100, message = "从域名长度不能超过100字符")
private String fromDomain;
/**
* 注册
*/
@NotNull(message = "注册标志不能为空")
private Boolean register = true;
/**
* 注册传输协议
*/
@Size(max = 20, message = "注册传输协议长度不能超过20字符")
private String registerTransport = "udp";
/**
* 是否启用
*/
@NotNull(message = "启用标志不能为空")
private Boolean enabled = true;
/**
* 扩展配置JSON格式
*/
private String configJson;
/**
* 备注
*/
@Size(max = 500, message = "备注长度不能超过500字符")
private String remarks;
}

View File

@@ -0,0 +1,225 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-09 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-09 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.gateway;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* FreeSwitch网关服务类
* 处理SIP网关配置、状态监控、连接管理等业务逻辑
*/
@Slf4j
@Service
@AllArgsConstructor
@ConditionalOnProperty(name = "bytedesk.freeswitch.enabled", havingValue = "true")
public class FreeSwitchGatewayService {
private final FreeSwitchGatewayRepository gatewayRepository;
/**
* 创建新网关
*/
@Transactional
public FreeSwitchGatewayEntity createGateway(String gatewayName, String description, String proxy,
String username, String password, String fromUser,
String fromDomain, Boolean register, String registerTransport) {
FreeSwitchGatewayEntity gateway = FreeSwitchGatewayEntity.builder()
.gatewayName(gatewayName)
.description(description)
.proxy(proxy)
.username(username)
.password(password)
.fromUser(fromUser)
.fromDomain(fromDomain)
.register(register != null ? register : true)
.registerTransport(registerTransport != null ? registerTransport : "udp")
.enabled(true)
.status("DOWN")
.build();
FreeSwitchGatewayEntity saved = gatewayRepository.save(gateway);
log.info("创建网关: {} (ID: {}) -> {}", gatewayName, saved.getId(), proxy);
return saved;
}
/**
* 根据ID查找网关
*/
public Optional<FreeSwitchGatewayEntity> findById(Long id) {
return gatewayRepository.findById(id);
}
/**
* 根据名称查找网关
*/
public Optional<FreeSwitchGatewayEntity> findByGatewayName(String gatewayName) {
return gatewayRepository.findByGatewayName(gatewayName);
}
/**
* 获取所有启用的网关
*/
public List<FreeSwitchGatewayEntity> findEnabledGateways() {
return gatewayRepository.findByEnabledTrue();
}
/**
* 获取所有网关(分页)
*/
public Page<FreeSwitchGatewayEntity> findAll(Pageable pageable) {
return gatewayRepository.findAll(pageable);
}
/**
* 根据状态查找网关
*/
public List<FreeSwitchGatewayEntity> findByStatus(String status) {
return gatewayRepository.findByStatus(status);
}
/**
* 更新网关信息
*/
@Transactional
public FreeSwitchGatewayEntity updateGateway(Long id, String description, String proxy,
String username, String password, String fromUser,
String fromDomain, Boolean register, String registerTransport) {
FreeSwitchGatewayEntity gateway = gatewayRepository.findById(id)
.orElseThrow(() -> new RuntimeException("网关不存在: " + id));
if (description != null) gateway.setDescription(description);
if (proxy != null) gateway.setProxy(proxy);
if (username != null) gateway.setUsername(username);
if (password != null) gateway.setPassword(password);
if (fromUser != null) gateway.setFromUser(fromUser);
if (fromDomain != null) gateway.setFromDomain(fromDomain);
if (register != null) gateway.setRegister(register);
if (registerTransport != null) gateway.setRegisterTransport(registerTransport);
FreeSwitchGatewayEntity saved = gatewayRepository.save(gateway);
log.info("更新网关: {} (ID: {})", gateway.getGatewayName(), id);
return saved;
}
/**
* 启用网关
*/
@Transactional
public void enableGateway(Long id) {
FreeSwitchGatewayEntity gateway = gatewayRepository.findById(id)
.orElseThrow(() -> new RuntimeException("网关不存在: " + id));
gateway.setEnabled(true);
gatewayRepository.save(gateway);
log.info("启用网关: {} (ID: {})", gateway.getGatewayName(), id);
}
/**
* 禁用网关
*/
@Transactional
public void disableGateway(Long id) {
FreeSwitchGatewayEntity gateway = gatewayRepository.findById(id)
.orElseThrow(() -> new RuntimeException("网关不存在: " + id));
gateway.setEnabled(false);
gatewayRepository.save(gateway);
log.info("禁用网关: {} (ID: {})", gateway.getGatewayName(), id);
}
/**
* 更新网关状态
*/
@Transactional
public void updateGatewayStatus(Long id, String status) {
FreeSwitchGatewayEntity gateway = gatewayRepository.findById(id)
.orElseThrow(() -> new RuntimeException("网关不存在: " + id));
gateway.setStatus(status);
gatewayRepository.save(gateway);
log.debug("更新网关状态: {} (ID: {}) -> {}", gateway.getGatewayName(), id, status);
}
/**
* 根据网关名称更新状态
*/
@Transactional
public void updateGatewayStatusByName(String gatewayName, String status) {
Optional<FreeSwitchGatewayEntity> gatewayOpt = gatewayRepository.findByGatewayName(gatewayName);
if (gatewayOpt.isPresent()) {
FreeSwitchGatewayEntity gateway = gatewayOpt.get();
gateway.setStatus(status);
gatewayRepository.save(gateway);
log.debug("更新网关状态: {} -> {}", gatewayName, status);
} else {
log.warn("网关不存在: {}", gatewayName);
}
}
/**
* 删除网关
*/
@Transactional
public void deleteGateway(Long id) {
FreeSwitchGatewayEntity gateway = gatewayRepository.findById(id)
.orElseThrow(() -> new RuntimeException("网关不存在: " + id));
gatewayRepository.delete(gateway);
log.info("删除网关: {} (ID: {})", gateway.getGatewayName(), id);
}
/**
* 获取所有在线的网关
*/
public List<FreeSwitchGatewayEntity> findOnlineGateways() {
return gatewayRepository.findByStatusAndEnabledTrue("UP");
}
/**
* 获取网关总数
*/
public long countTotal() {
return gatewayRepository.count();
}
/**
* 获取启用的网关数量
*/
public long countEnabled() {
return gatewayRepository.countByEnabledTrue();
}
/**
* 获取在线的网关数量
*/
public long countOnline() {
return gatewayRepository.countByStatusAndEnabledTrue("UP");
}
/**
* 检查网关名称是否存在
*/
public boolean existsByGatewayName(String gatewayName) {
return gatewayRepository.existsByGatewayName(gatewayName);
}
}

View File

@@ -0,0 +1,342 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-09 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-09 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.user;
import com.bytedesk.core.utils.JsonResult;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* FreeSwitch用户管理REST API控制器
*/
@Slf4j
@RestController
@RequestMapping("/freeswitch/api/v1/users")
@AllArgsConstructor
@ConditionalOnProperty(name = "bytedesk.freeswitch.enabled", havingValue = "true")
public class FreeSwitchUserController {
private final FreeSwitchUserService userService;
/**
* 创建用户
*/
@PostMapping
public ResponseEntity<JsonResult<?>> createUser(@Valid @RequestBody FreeSwitchUserRequest request) {
try {
FreeSwitchUserEntity user = userService.createUser(
request.getUsername(),
request.getDomain(),
request.getPassword(),
request.getDisplayName(),
request.getEmail(),
request.getAccountcode()
);
FreeSwitchUserResponse response = FreeSwitchUserResponse.fromEntitySafe(user);
return ResponseEntity.ok(JsonResult.success("用户创建成功", response));
} catch (Exception e) {
log.error("创建用户失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 获取用户详情
*/
@GetMapping("/{id}")
public ResponseEntity<JsonResult<?>> getUser(@PathVariable Long id) {
Optional<FreeSwitchUserEntity> user = userService.findById(id);
if (user.isPresent()) {
FreeSwitchUserResponse response = FreeSwitchUserResponse.fromEntitySafe(user.get());
return ResponseEntity.ok(JsonResult.success("获取用户详情成功", response));
} else {
return ResponseEntity.notFound().build();
}
}
/**
* 根据用户名获取用户
*/
@GetMapping("/username/{username}")
public ResponseEntity<JsonResult<?>> getUserByUsername(@PathVariable String username) {
Optional<FreeSwitchUserEntity> user = userService.findByUsername(username);
if (user.isPresent()) {
FreeSwitchUserResponse response = FreeSwitchUserResponse.fromEntitySafe(user.get());
return ResponseEntity.ok(JsonResult.success("获取用户详情成功", response));
} else {
return ResponseEntity.notFound().build();
}
}
/**
* 根据用户名和域名获取用户
*/
@GetMapping("/username/{username}/domain/{domain}")
public ResponseEntity<JsonResult<?>> getUserByUsernameAndDomain(
@PathVariable String username, @PathVariable String domain) {
Optional<FreeSwitchUserEntity> user = userService.findByUsernameAndDomain(username, domain);
if (user.isPresent()) {
FreeSwitchUserResponse response = FreeSwitchUserResponse.fromEntitySafe(user.get());
return ResponseEntity.ok(JsonResult.success("获取用户详情成功", response));
} else {
return ResponseEntity.notFound().build();
}
}
/**
* 获取用户列表(分页)
*/
@GetMapping
public ResponseEntity<JsonResult<?>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "desc") String sortDir) {
Sort sort = sortDir.equalsIgnoreCase("desc") ?
Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<FreeSwitchUserEntity> userPage = userService.findAll(pageable);
Page<FreeSwitchUserResponse> responsePage = userPage.map(FreeSwitchUserResponse::fromEntitySafe);
return ResponseEntity.ok(JsonResult.success("获取用户列表成功", responsePage));
}
/**
* 根据域名获取用户列表
*/
@GetMapping("/domain/{domain}")
public ResponseEntity<JsonResult<?>> getUsersByDomain(@PathVariable String domain) {
List<FreeSwitchUserEntity> users = userService.findByDomain(domain);
List<FreeSwitchUserResponse> responses = users.stream()
.map(FreeSwitchUserResponse::fromEntitySafe)
.collect(Collectors.toList());
return ResponseEntity.ok(JsonResult.success("获取域名用户列表成功", responses));
}
/**
* 获取启用的用户列表
*/
@GetMapping("/enabled")
public ResponseEntity<JsonResult<?>> getEnabledUsers() {
List<FreeSwitchUserEntity> users = userService.findEnabledUsers();
List<FreeSwitchUserResponse> responses = users.stream()
.map(FreeSwitchUserResponse::fromEntitySafe)
.collect(Collectors.toList());
return ResponseEntity.ok(JsonResult.success("获取启用用户列表成功", responses));
}
/**
* 获取在线的用户列表
*/
@GetMapping("/online")
public ResponseEntity<JsonResult<?>> getOnlineUsers() {
List<FreeSwitchUserEntity> users = userService.findOnlineUsers();
List<FreeSwitchUserResponse> responses = users.stream()
.map(FreeSwitchUserResponse::fromEntitySafe)
.collect(Collectors.toList());
return ResponseEntity.ok(JsonResult.success("获取在线用户列表成功", responses));
}
/**
* 根据邮箱获取用户列表
*/
@GetMapping("/email/{email}")
public ResponseEntity<JsonResult<?>> getUsersByEmail(@PathVariable String email) {
List<FreeSwitchUserEntity> users = userService.findByEmail(email);
List<FreeSwitchUserResponse> responses = users.stream()
.map(FreeSwitchUserResponse::fromEntitySafe)
.collect(Collectors.toList());
return ResponseEntity.ok(JsonResult.success("获取用户列表成功", responses));
}
/**
* 根据账户代码获取用户列表
*/
@GetMapping("/accountcode/{accountcode}")
public ResponseEntity<JsonResult<?>> getUsersByAccountcode(@PathVariable String accountcode) {
List<FreeSwitchUserEntity> users = userService.findByAccountcode(accountcode);
List<FreeSwitchUserResponse> responses = users.stream()
.map(FreeSwitchUserResponse::fromEntitySafe)
.collect(Collectors.toList());
return ResponseEntity.ok(JsonResult.success("获取用户列表成功", responses));
}
/**
* 更新用户
*/
@PutMapping("/{id}")
public ResponseEntity<JsonResult<?>> updateUser(
@PathVariable Long id,
@Valid @RequestBody FreeSwitchUserRequest request) {
try {
FreeSwitchUserEntity user = userService.updateUser(
id,
request.getPassword(),
request.getDisplayName(),
request.getEmail(),
request.getAccountcode()
);
FreeSwitchUserResponse response = FreeSwitchUserResponse.fromEntitySafe(user);
return ResponseEntity.ok(JsonResult.success("用户更新成功", response));
} catch (Exception e) {
log.error("更新用户失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 启用用户
*/
@PutMapping("/{id}/enable")
public ResponseEntity<JsonResult<?>> enableUser(@PathVariable Long id) {
try {
userService.enableUser(id);
return ResponseEntity.ok(JsonResult.success("用户启用成功"));
} catch (Exception e) {
log.error("启用用户失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 禁用用户
*/
@PutMapping("/{id}/disable")
public ResponseEntity<JsonResult<?>> disableUser(@PathVariable Long id) {
try {
userService.disableUser(id);
return ResponseEntity.ok(JsonResult.success("用户禁用成功"));
} catch (Exception e) {
log.error("禁用用户失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 更新用户注册信息
*/
@PutMapping("/registration")
public ResponseEntity<JsonResult<?>> updateUserRegistration(
@RequestParam String username,
@RequestParam String domain,
@RequestParam String registerIp,
@RequestParam(required = false) String userAgent) {
try {
userService.updateUserRegistration(username, domain, registerIp, userAgent);
return ResponseEntity.ok(JsonResult.success("用户注册信息更新成功"));
} catch (Exception e) {
log.error("更新用户注册信息失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public ResponseEntity<JsonResult<?>> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return ResponseEntity.ok(JsonResult.success("用户删除成功"));
} catch (Exception e) {
log.error("删除用户失败", e);
return ResponseEntity.badRequest().body(JsonResult.error(e.getMessage()));
}
}
/**
* 验证用户密码
*/
@PostMapping("/validate")
public ResponseEntity<JsonResult<?>> validateUserPassword(
@RequestParam String username,
@RequestParam String domain,
@RequestParam String password) {
boolean valid = userService.validateUserPassword(username, domain, password);
return ResponseEntity.ok(JsonResult.success("密码验证完成", valid));
}
/**
* 获取用户统计信息
*/
@GetMapping("/stats")
public ResponseEntity<JsonResult<?>> getUserStats() {
long totalCount = userService.countTotal();
long enabledCount = userService.countEnabled();
long onlineCount = userService.countOnline();
UserStats stats = new UserStats(totalCount, enabledCount, onlineCount);
return ResponseEntity.ok(JsonResult.success("获取用户统计成功", stats));
}
/**
* 根据域名获取用户统计信息
*/
@GetMapping("/stats/domain/{domain}")
public ResponseEntity<JsonResult<?>> getUserStatsByDomain(@PathVariable String domain) {
long domainCount = userService.countByDomain(domain);
return ResponseEntity.ok(JsonResult.success("获取域名用户统计成功", domainCount));
}
/**
* 检查用户名和域名组合是否存在
*/
@GetMapping("/exists")
public ResponseEntity<JsonResult<?>> checkUserExists(
@RequestParam String username,
@RequestParam String domain) {
boolean exists = userService.existsByUsernameAndDomain(username, domain);
return ResponseEntity.ok(JsonResult.success("检查用户名成功", exists));
}
/**
* 用户统计信息内部类
*/
public static class UserStats {
private final long totalCount;
private final long enabledCount;
private final long onlineCount;
public UserStats(long totalCount, long enabledCount, long onlineCount) {
this.totalCount = totalCount;
this.enabledCount = enabledCount;
this.onlineCount = onlineCount;
}
public long getTotalCount() { return totalCount; }
public long getEnabledCount() { return enabledCount; }
public long getOnlineCount() { return onlineCount; }
}
}

View File

@@ -0,0 +1,85 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-09 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-09 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* FreeSwitch用户请求实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FreeSwitchUserRequest {
/**
* 用户名SIP用户名
*/
@NotBlank(message = "用户名不能为空")
@Size(max = 50, message = "用户名长度不能超过50字符")
private String username;
/**
* SIP域名
*/
@NotBlank(message = "SIP域名不能为空")
@Size(max = 100, message = "SIP域名长度不能超过100字符")
private String domain;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Size(max = 255, message = "密码长度不能超过255字符")
private String password;
/**
* 显示名称
*/
@Size(max = 100, message = "显示名称长度不能超过100字符")
private String displayName;
/**
* 邮箱
*/
@Email(message = "邮箱格式不正确")
@Size(max = 100, message = "邮箱长度不能超过100字符")
private String email;
/**
* 账户代码
*/
@Size(max = 50, message = "账户代码长度不能超过50字符")
private String accountcode;
/**
* 是否启用
*/
@NotNull(message = "启用标志不能为空")
private Boolean enabled = true;
/**
* 备注
*/
@Size(max = 500, message = "备注长度不能超过500字符")
private String remarks;
}

View File

@@ -0,0 +1,138 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-09 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-09 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* FreeSwitch用户响应实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FreeSwitchUserResponse {
/**
* ID
*/
private Long id;
/**
* 用户名SIP用户名
*/
private String username;
/**
* SIP域名
*/
private String domain;
/**
* 显示名称
*/
private String displayName;
/**
* 邮箱
*/
private String email;
/**
* 账户代码
*/
private String accountcode;
/**
* 是否启用
*/
private Boolean enabled;
/**
* 最后注册时间
*/
private LocalDateTime lastRegister;
/**
* 注册IP地址
*/
private String registerIp;
/**
* 用户代理
*/
private String userAgent;
/**
* 备注
*/
private String remarks;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 更新时间
*/
private LocalDateTime updatedAt;
/**
* SIP地址
*/
private String sipAddress;
/**
* 是否在线
*/
private Boolean online;
/**
* 从实体创建响应对象
*/
public static FreeSwitchUserResponse fromEntity(FreeSwitchUserEntity entity) {
return FreeSwitchUserResponse.builder()
.id(entity.getId())
.username(entity.getUsername())
.domain(entity.getDomain())
.displayName(entity.getDisplayName())
.email(entity.getEmail())
.accountcode(entity.getAccountcode())
.enabled(entity.getEnabled())
.lastRegister(entity.getLastRegister())
.registerIp(entity.getRegisterIp())
.userAgent(entity.getUserAgent())
.remarks(entity.getRemarks())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.sipAddress(entity.getSipAddress())
.online(entity.isOnline())
.build();
}
/**
* 从实体创建响应对象(隐藏密码)
*/
public static FreeSwitchUserResponse fromEntitySafe(FreeSwitchUserEntity entity) {
FreeSwitchUserResponse response = fromEntity(entity);
// 不返回密码信息
return response;
}
}

View File

@@ -0,0 +1,254 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-06-09 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-09 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.freeswitch.user;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* FreeSwitch用户服务类
* 处理SIP用户管理、注册状态监控等业务逻辑
*/
@Slf4j
@Service
@AllArgsConstructor
@ConditionalOnProperty(name = "bytedesk.freeswitch.enabled", havingValue = "true")
public class FreeSwitchUserService {
private final FreeSwitchUserRepository userRepository;
/**
* 创建新用户
*/
@Transactional
public FreeSwitchUserEntity createUser(String username, String domain, String password,
String displayName, String email, String accountcode) {
FreeSwitchUserEntity user = FreeSwitchUserEntity.builder()
.username(username)
.domain(domain)
.password(password)
.displayName(displayName)
.email(email)
.accountcode(accountcode)
.enabled(true)
.build();
FreeSwitchUserEntity saved = userRepository.save(user);
log.info("创建用户: {} (ID: {}) -> {}@{}", username, saved.getId(), username, domain);
return saved;
}
/**
* 根据ID查找用户
*/
public Optional<FreeSwitchUserEntity> findById(Long id) {
return userRepository.findById(id);
}
/**
* 根据用户名查找用户
*/
public Optional<FreeSwitchUserEntity> findByUsername(String username) {
return userRepository.findByUsername(username);
}
/**
* 根据用户名和域名查找用户
*/
public Optional<FreeSwitchUserEntity> findByUsernameAndDomain(String username, String domain) {
return userRepository.findByUsernameAndDomain(username, domain);
}
/**
* 获取指定域名的用户列表
*/
public List<FreeSwitchUserEntity> findByDomain(String domain) {
return userRepository.findByDomain(domain);
}
/**
* 获取所有启用的用户
*/
public List<FreeSwitchUserEntity> findEnabledUsers() {
return userRepository.findByEnabledTrue();
}
/**
* 获取所有用户(分页)
*/
public Page<FreeSwitchUserEntity> findAll(Pageable pageable) {
return userRepository.findAll(pageable);
}
/**
* 根据域名查找用户(分页)
*/
public Page<FreeSwitchUserEntity> findByDomain(String domain, Pageable pageable) {
return userRepository.findByDomain(domain, pageable);
}
/**
* 更新用户信息
*/
@Transactional
public FreeSwitchUserEntity updateUser(Long id, String password, String displayName,
String email, String accountcode) {
FreeSwitchUserEntity user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在: " + id));
if (password != null) user.setPassword(password);
if (displayName != null) user.setDisplayName(displayName);
if (email != null) user.setEmail(email);
if (accountcode != null) user.setAccountcode(accountcode);
FreeSwitchUserEntity saved = userRepository.save(user);
log.info("更新用户: {} (ID: {})", user.getUsername(), id);
return saved;
}
/**
* 启用用户
*/
@Transactional
public void enableUser(Long id) {
FreeSwitchUserEntity user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在: " + id));
user.setEnabled(true);
userRepository.save(user);
log.info("启用用户: {} (ID: {})", user.getUsername(), id);
}
/**
* 禁用用户
*/
@Transactional
public void disableUser(Long id) {
FreeSwitchUserEntity user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在: " + id));
user.setEnabled(false);
userRepository.save(user);
log.info("禁用用户: {} (ID: {})", user.getUsername(), id);
}
/**
* 更新用户注册信息
*/
@Transactional
public void updateUserRegistration(String username, String domain, String registerIp, String userAgent) {
Optional<FreeSwitchUserEntity> userOpt = userRepository.findByUsernameAndDomain(username, domain);
if (userOpt.isPresent()) {
FreeSwitchUserEntity user = userOpt.get();
user.setLastRegister(LocalDateTime.now());
user.setRegisterIp(registerIp);
user.setUserAgent(userAgent);
userRepository.save(user);
log.debug("更新用户注册信息: {}@{} from {}", username, domain, registerIp);
} else {
log.warn("用户不存在: {}@{}", username, domain);
}
}
/**
* 删除用户
*/
@Transactional
public void deleteUser(Long id) {
FreeSwitchUserEntity user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在: " + id));
userRepository.delete(user);
log.info("删除用户: {} (ID: {})", user.getUsername(), id);
}
/**
* 获取在线用户列表
*/
public List<FreeSwitchUserEntity> findOnlineUsers() {
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(5);
return userRepository.findByEnabledTrueAndLastRegisterAfter(cutoffTime);
}
/**
* 根据邮箱查找用户
*/
public List<FreeSwitchUserEntity> findByEmail(String email) {
return userRepository.findByEmail(email);
}
/**
* 根据账户代码查找用户
*/
public List<FreeSwitchUserEntity> findByAccountcode(String accountcode) {
return userRepository.findByAccountcode(accountcode);
}
/**
* 获取用户总数
*/
public long countTotal() {
return userRepository.count();
}
/**
* 获取启用的用户数量
*/
public long countEnabled() {
return userRepository.countByEnabledTrue();
}
/**
* 获取在线的用户数量
*/
public long countOnline() {
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(5);
return userRepository.countByEnabledTrueAndLastRegisterAfter(cutoffTime);
}
/**
* 根据域名统计用户数量
*/
public long countByDomain(String domain) {
return userRepository.countByDomain(domain);
}
/**
* 检查用户名和域名组合是否存在
*/
public boolean existsByUsernameAndDomain(String username, String domain) {
return userRepository.existsByUsernameAndDomain(username, domain);
}
/**
* 验证用户密码
*/
public boolean validateUserPassword(String username, String domain, String password) {
Optional<FreeSwitchUserEntity> userOpt = userRepository.findByUsernameAndDomain(username, domain);
if (userOpt.isPresent()) {
FreeSwitchUserEntity user = userOpt.get();
return user.getEnabled() && user.getPassword().equals(password);
}
return false;
}
}