This commit is contained in:
jack ning
2025-11-15 13:38:18 +08:00
parent ec097e9601
commit 3bafe63e1c
12 changed files with 251 additions and 85 deletions

View File

@@ -1 +1 @@
# Call Center
# Bytedesk AI Call Center

View File

@@ -1 +1 @@
# 微语智能呼叫中心系统
# 微语AI智能呼叫中心系统

View File

@@ -88,6 +88,12 @@ public class TicketEntity extends BaseEntity {
@Builder.Default
private String priority = TicketPriorityEnum.LOW.name(); // 优先级(低/中/高/紧急)
/**
* Human friendly ticket number generated from ticket settings
*/
// @Column(name = "ticket_number", length = 64, unique = true)
private String ticketNumber;
/**
* Type of ticket (AGENT, GROUP)
*/

View File

@@ -23,9 +23,13 @@ import lombok.Data;
@Data
public class TicketExcel {
@ExcelProperty(value = "标题")
// @ExcelProperty(value = "标题")
// @ColumnWidth(20)
// private String title;
@ExcelProperty(value = "工单编号")
@ColumnWidth(20)
private String title;
private String ticketNumber;
@ExcelProperty(value = "描述")
@ColumnWidth(20)

View File

@@ -24,6 +24,10 @@ public interface TicketRepository extends JpaRepository<TicketEntity, Long>, Jpa
Optional<TicketEntity> findByUid(String uid);
Optional<TicketEntity> findByTicketNumber(String ticketNumber);
boolean existsByTicketNumber(String ticketNumber);
Optional<TicketEntity> findByProcessInstanceId(String processInstanceId);
List<TicketEntity> findByWorkgroupUidContainingAndCreatedAtBetween(

View File

@@ -44,6 +44,8 @@ public class TicketRequest extends BaseRequest {
private String phone;
// 联系邮箱
private String email;
private String ticketNumber;
// private String searchText;
//
private String status;

View File

@@ -41,6 +41,8 @@ public class TicketResponse extends BaseResponse {
private String contactName;
private String phone;
private String email;
private String ticketNumber;
//
private String status;
private String priority;

View File

@@ -53,6 +53,9 @@ import com.bytedesk.ticket.attachment.TicketAttachmentRepository;
import com.bytedesk.ticket.ticket.event.TicketUpdateAssigneeEvent;
import com.bytedesk.ticket.ticket.event.TicketUpdateDepartmentEvent;
import com.bytedesk.ticket.utils.TicketConvertUtils;
import com.bytedesk.ticket.ticket_settings.TicketSettingsResponse;
import com.bytedesk.ticket.ticket_settings.TicketSettingsRestService;
import com.bytedesk.ticket.ticket_settings.sub.dto.TicketBasicSettingsResponse;
import com.bytedesk.core.topic.TopicUtils;
import lombok.AllArgsConstructor;
@@ -82,6 +85,8 @@ public class TicketRestService
private final CategoryRestService categoryRestService;
private final TicketSettingsRestService ticketSettingsRestService;
@Cacheable(value = "ticket", key = "#uid", unless = "#result == null")
@Override
public Optional<TicketEntity> findByUid(String uid) {
@@ -117,6 +122,8 @@ public class TicketRestService
ticket.setStatus(TicketStatusEnum.NEW.name());
}
ticket.setReporter(request.getReporterJson());
ensureTicketNumber(ticket, request);
// 先保存工单
TicketEntity savedTicket = save(ticket);
// 保存附件
@@ -361,4 +368,104 @@ public class TicketRestService
return ticketRepository.findAll(specification, pageable);
}
private void ensureTicketNumber(TicketEntity ticket, TicketRequest request) {
if (ticket == null || StringUtils.hasText(ticket.getTicketNumber())) {
return;
}
String orgUid = resolveOrgUid(ticket, request);
String workgroupUid = resolveWorkgroupUid(ticket, request);
ticket.setTicketNumber(generateTicketNumber(orgUid, workgroupUid));
}
private String generateTicketNumber(String orgUid, String workgroupUid) {
TicketBasicSettingsResponse basicSettings = fetchBasicSettings(orgUid, workgroupUid);
String prefix = resolvePrefix(basicSettings);
int numericLength = resolveNumericLength(prefix, basicSettings);
for (int i = 0; i < 5; i++) {
String candidate = prefix + buildNumericPart(numericLength);
if (!ticketRepository.existsByTicketNumber(candidate)) {
return candidate;
}
}
return prefix + uidUtils.getUid();
}
private TicketBasicSettingsResponse fetchBasicSettings(String orgUid, String workgroupUid) {
if (!StringUtils.hasText(orgUid) || !StringUtils.hasText(workgroupUid)) {
return null;
}
try {
TicketSettingsResponse settings = ticketSettingsRestService.getOrDefaultByWorkgroup(orgUid, workgroupUid);
if (settings == null) {
return null;
}
return settings.getBasicSettings() != null
? settings.getBasicSettings()
: settings.getDraftBasicSettings();
} catch (Exception ex) {
log.warn("Failed to load ticket settings for org {} workgroup {}: {}", orgUid, workgroupUid, ex.getMessage());
return null;
}
}
private String resolvePrefix(TicketBasicSettingsResponse basicSettings) {
if (basicSettings != null && StringUtils.hasText(basicSettings.getNumberPrefix())) {
return basicSettings.getNumberPrefix().trim().toUpperCase();
}
return "TK";
}
private int resolveNumericLength(String prefix, TicketBasicSettingsResponse basicSettings) {
int prefixLength = StringUtils.hasText(prefix) ? prefix.length() : 0;
Integer configuredLength = basicSettings != null ? basicSettings.getNumberLength() : null;
int totalLength = (configuredLength != null && configuredLength > prefixLength)
? configuredLength
: prefixLength + 8;
int numericLength = totalLength - prefixLength;
numericLength = Math.max(numericLength, 4);
return Math.min(numericLength, 32);
}
private String buildNumericPart(int length) {
String raw = uidUtils.getUid();
if (length <= 0) {
return raw;
}
if (raw.length() > length) {
return raw.substring(raw.length() - length);
}
StringBuilder builder = new StringBuilder();
for (int i = raw.length(); i < length; i++) {
builder.append('0');
}
builder.append(raw);
return builder.toString();
}
private String resolveOrgUid(TicketEntity ticket, TicketRequest request) {
if (ticket != null && StringUtils.hasText(ticket.getOrgUid())) {
return ticket.getOrgUid();
}
if (request != null && StringUtils.hasText(request.getOrgUid())) {
return request.getOrgUid();
}
UserEntity user = authService.getUser();
if (user != null && StringUtils.hasText(user.getOrgUid())) {
return user.getOrgUid();
}
return BytedeskConsts.DEFAULT_ORGANIZATION_UID;
}
private String resolveWorkgroupUid(TicketEntity ticket, TicketRequest request) {
if (ticket != null && StringUtils.hasText(ticket.getWorkgroupUid())) {
return ticket.getWorkgroupUid();
}
if (request != null && StringUtils.hasText(request.getWorkgroupUid())) {
return request.getWorkgroupUid();
}
if (ticket != null && StringUtils.hasText(ticket.getDepartmentUid())) {
return ticket.getDepartmentUid();
}
return BytedeskConsts.DEFAULT_WORKGROUP_UID;
}
}

View File

@@ -49,6 +49,10 @@ public class TicketSpecification extends BaseSpecification<TicketEntity, TicketR
if (StringUtils.hasText(request.getTopic())) {
predicates.add(criteriaBuilder.like(root.get("topic"), "%" + request.getTopic() + "%"));
}
// ticket number
if (StringUtils.hasText(request.getTicketNumber())) {
predicates.add(criteriaBuilder.equal(root.get("ticketNumber"), request.getTicketNumber()));
}
// status
if (StringUtils.hasText(request.getStatus())) {
predicates.add(criteriaBuilder.equal(root.get("status"), request.getStatus()));

View File

@@ -17,6 +17,7 @@ import java.time.ZonedDateTime;
import com.bytedesk.core.base.BaseEntity;
import com.bytedesk.core.constant.I18Consts;
import com.bytedesk.ticket.ticket_settings.sub.TicketBasicSettingsEntity;
import com.bytedesk.ticket.ticket_settings.sub.TicketCategorySettingsEntity;
import jakarta.persistence.CascadeType;
@@ -81,8 +82,8 @@ public class TicketSettingsEntity extends BaseEntity {
//
// ====== 发布版本 ======
// @ManyToOne(fetch = FetchType.LAZY, optional = true, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
// private TicketBasicSettingsEntity basicSettings;
@ManyToOne(fetch = FetchType.LAZY, optional = true, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
private TicketBasicSettingsEntity basicSettings;
@ManyToOne(fetch = FetchType.LAZY, optional = true, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
private TicketCategorySettingsEntity categorySettings;
@@ -103,8 +104,8 @@ public class TicketSettingsEntity extends BaseEntity {
// private TicketCustomFieldSettingsEntity customFieldSettings;
// ====== 草稿版本 ======
// @ManyToOne(fetch = FetchType.LAZY, optional = true, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
// private TicketBasicSettingsEntity draftBasicSettings;
@ManyToOne(fetch = FetchType.LAZY, optional = true, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
private TicketBasicSettingsEntity draftBasicSettings;
@ManyToOne(fetch = FetchType.LAZY, optional = true, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
private TicketCategorySettingsEntity draftCategorySettings;

View File

@@ -54,6 +54,7 @@ public class TicketSettingsRequest extends BaseRequest {
// 草稿子配置(仅在更新草稿时传入;创建时也可传,后台将初始化发布+草稿)
private TicketBasicSettingsRequest basicSettings;
private TicketBasicSettingsRequest draftBasicSettings;
private TicketStatusFlowSettingsRequest statusFlowSettings;
private TicketPrioritySettingsRequest prioritySettings;
private TicketAssignmentSettingsRequest assignmentSettings;

View File

@@ -34,7 +34,10 @@ import com.bytedesk.core.rbac.user.UserEntity;
import com.bytedesk.core.uid.UidUtils;
import com.bytedesk.ticket.ticket_settings.binding.TicketSettingsBindingEntity;
import com.bytedesk.ticket.ticket_settings.binding.TicketSettingsBindingRepository;
import com.bytedesk.ticket.ticket_settings.sub.TicketBasicSettingsEntity;
import com.bytedesk.ticket.ticket_settings.sub.TicketCategorySettingsEntity;
import com.bytedesk.ticket.ticket_settings.sub.dto.TicketBasicSettingsRequest;
import com.bytedesk.ticket.ticket_settings.sub.dto.TicketBasicSettingsResponse;
import com.bytedesk.ticket.ticket_settings.sub.dto.TicketCategorySettingsRequest;
import com.bytedesk.ticket.ticket_settings.sub.dto.TicketCategorySettingsResponse;
import com.bytedesk.ticket.ticket_settings.sub.dto.TicketCategoryItemResponse;
@@ -111,14 +114,12 @@ public class TicketSettingsRestService extends
entity.setUid(uidUtils.getUid());
}
// 初始化并绑定发布 + 草稿子配置,参考 WorkgroupSettings 的 create 逻辑
// Basic
// TicketBasicSettingsEntity basic = TicketBasicSettingsEntity.fromRequest(request.getBasicSettings(), modelMapper);
// basic.setUid(uidUtils.getUid());
// entity.setBasicSettings(basic);
// TicketBasicSettingsEntity draftBasic = TicketBasicSettingsEntity.fromRequest(request.getBasicSettings(), modelMapper);
// draftBasic.setUid(uidUtils.getUid());
// entity.setDraftBasicSettings(draftBasic);
// 初始化并绑定发布 + 草稿子配置
TicketBasicSettingsEntity basic = createBasicSettingsEntity(request.getBasicSettings(), entity.getOrgUid());
entity.setBasicSettings(basic);
TicketBasicSettingsEntity draftBasic = createBasicSettingsEntity(resolveDraftBasicRequest(request), entity.getOrgUid());
entity.setDraftBasicSettings(draftBasic);
TicketCategorySettingsEntity category = TicketCategorySettingsEntity
.fromRequest(request.getCategorySettings(), uidUtils::getUid);
@@ -218,22 +219,17 @@ public class TicketSettingsRestService extends
modelMapper.map(request, entity);
boolean draftUpdated = false;
// // 仅更新草稿子配置:与 WorkgroupSettingsRestService.update 一致
// if (request.getDraftBasicSettings() != null) {
// TicketBasicSettingsEntity draft = entity.getDraftBasicSettings();
// if (draft == null) {
// draft = TicketBasicSettingsEntity.fromRequest(request.getDraftBasicSettings(), modelMapper);
// draft.setUid(uidUtils.getUid());
// entity.setDraftBasicSettings(draft);
// } else {
// String originalUid = draft.getUid();
// TicketBasicSettingsEntity tmp = TicketBasicSettingsEntity
// .fromRequest(request.getBasicSettings(), modelMapper);
// modelMapper.map(tmp, draft);
// draft.setUid(originalUid);
// }
// draftUpdated = true;
// }
TicketBasicSettingsRequest draftBasicRequest = resolveDraftBasicRequest(request);
if (draftBasicRequest != null) {
TicketBasicSettingsEntity draft = entity.getDraftBasicSettings();
if (draft == null) {
draft = createBasicSettingsEntity(draftBasicRequest, entity.getOrgUid());
entity.setDraftBasicSettings(draft);
} else {
applyBasicSettingsRequest(draft, draftBasicRequest);
}
draftUpdated = true;
}
if (request.getDraftCategorySettings() != null) {
TicketCategorySettingsEntity draftCategory = entity.getDraftCategorySettings();
@@ -405,14 +401,10 @@ public class TicketSettingsRestService extends
draftCategory.setUid(uidUtils.getUid());
settings.setDraftCategorySettings(draftCategory);
// // 依照 WorkgroupSettingsRestService 排列方式:每种子配置“发布 + 草稿”成对紧邻,便于阅读与维护
// // Basic (published + draft)
// TicketBasicSettingsEntity basic = TicketBasicSettingsEntity.fromRequest(null, modelMapper);
// basic.setUid(uidUtils.getUid());
// settings.setBasicSettings(basic);
// TicketBasicSettingsEntity draftBasic = TicketBasicSettingsEntity.fromRequest(null, modelMapper);
// draftBasic.setUid(uidUtils.getUid());
// settings.setDraftBasicSettings(draftBasic);
TicketBasicSettingsEntity basic = createBasicSettingsEntity(null, orgUid);
settings.setBasicSettings(basic);
TicketBasicSettingsEntity draftBasic = createBasicSettingsEntity(null, orgUid);
settings.setDraftBasicSettings(draftBasic);
// // StatusFlow (published + draft)
// TicketStatusFlowSettingsEntity flow = TicketStatusFlowSettingsEntity.fromRequest(null);
@@ -543,21 +535,16 @@ public class TicketSettingsRestService extends
}
draftUpdated = true;
}
// if (request.getDraftBasicSettings() != null) {
// TicketBasicSettingsEntity draft = entity.getDraftBasicSettings();
// if (draft == null) {
// draft = TicketBasicSettingsEntity.fromRequest(request.getDraftBasicSettings(), modelMapper);
// draft.setUid(uidUtils.getUid());
// entity.setDraftBasicSettings(draft);
// } else {
// String originalUid = draft.getUid();
// TicketBasicSettingsEntity tmp = TicketBasicSettingsEntity
// .fromRequest(request.getDraftBasicSettings(), modelMapper);
// modelMapper.map(tmp, draft);
// draft.setUid(originalUid);
// }
// draftUpdated = true;
// }
if (request.getDraftBasicSettings() != null) {
TicketBasicSettingsEntity draft = entity.getDraftBasicSettings();
if (draft == null) {
draft = createBasicSettingsEntity(request.getDraftBasicSettings(), entity.getOrgUid());
entity.setDraftBasicSettings(draft);
} else {
applyBasicSettingsRequest(draft, request.getDraftBasicSettings());
}
draftUpdated = true;
}
// if (request.getDraftStatusFlowSettings() != null) {
// TicketStatusFlowSettingsEntity draft = entity.getDraftStatusFlowSettings();
// if (draft == null) {
@@ -658,14 +645,14 @@ public class TicketSettingsRestService extends
}
TicketSettingsEntity entity = optional.get();
// ===== 基础设置 =====
// if (entity.getDraftBasicSettings() != null) {
// if (entity.getBasicSettings() == null) {
// entity.setBasicSettings(cloneSettings(entity.getDraftBasicSettings()));
// } else {
// copyBusinessFields(entity.getDraftBasicSettings(), entity.getBasicSettings());
// }
// }
if (entity.getDraftBasicSettings() != null) {
TicketBasicSettingsEntity publishedBasic = entity.getBasicSettings();
if (publishedBasic == null) {
publishedBasic = createBasicSettingsEntity(null, entity.getOrgUid());
entity.setBasicSettings(publishedBasic);
}
copyBasicSettings(entity.getDraftBasicSettings(), publishedBasic);
}
if (entity.getDraftCategorySettings() != null) {
TicketCategorySettingsEntity draftCategory = entity.getDraftCategorySettings();
@@ -879,6 +866,74 @@ public class TicketSettingsRestService extends
: request.getCategorySettings();
}
private TicketBasicSettingsRequest resolveDraftBasicRequest(TicketSettingsRequest request) {
if (request == null) {
return null;
}
return request.getDraftBasicSettings() != null
? request.getDraftBasicSettings()
: request.getBasicSettings();
}
private TicketBasicSettingsEntity createBasicSettingsEntity(TicketBasicSettingsRequest request, String orgUid) {
TicketBasicSettingsEntity entity = TicketBasicSettingsEntity.fromRequest(request, modelMapper);
entity.setUid(uidUtils.getUid());
entity.setOrgUid(orgUid);
return entity;
}
private void applyBasicSettingsRequest(TicketBasicSettingsEntity target, TicketBasicSettingsRequest request) {
if (target == null || request == null) {
return;
}
if (request.getNumberPrefix() != null) {
target.setNumberPrefix(request.getNumberPrefix());
}
if (request.getNumberLength() != null) {
target.setNumberLength(request.getNumberLength());
}
if (request.getDefaultPriority() != null) {
target.setDefaultPriority(request.getDefaultPriority());
}
if (request.getValidityDays() != null) {
target.setValidityDays(request.getValidityDays());
}
if (request.getAutoCloseHours() != null) {
target.setAutoCloseHours(request.getAutoCloseHours());
}
if (request.getEnableAutoClose() != null) {
target.setEnableAutoClose(request.getEnableAutoClose());
}
}
private void copyBasicSettings(TicketBasicSettingsEntity source, TicketBasicSettingsEntity target) {
if (source == null || target == null) {
return;
}
target.setNumberPrefix(source.getNumberPrefix());
target.setNumberLength(source.getNumberLength());
target.setDefaultPriority(source.getDefaultPriority());
target.setValidityDays(source.getValidityDays());
target.setAutoCloseHours(source.getAutoCloseHours());
target.setEnableAutoClose(source.getEnableAutoClose());
target.setRequireLoginToCreate(source.getRequireLoginToCreate());
}
private TicketBasicSettingsResponse mapBasicSettings(TicketBasicSettingsEntity entity) {
if (entity == null) {
return null;
}
return TicketBasicSettingsResponse.builder()
.uid(entity.getUid())
.numberPrefix(entity.getNumberPrefix())
.numberLength(entity.getNumberLength())
.defaultPriority(entity.getDefaultPriority())
.validityDays(entity.getValidityDays())
.autoCloseHours(entity.getAutoCloseHours())
.enableAutoClose(entity.getEnableAutoClose())
.build();
}
private CategorySettingsData copyCategorySettings(CategorySettingsData source) {
if (source == null) {
CategorySettingsData copy = CategorySettingsData.builder().build();
@@ -934,17 +989,7 @@ public class TicketSettingsRestService extends
public TicketSettingsResponse convertToResponse(TicketSettingsEntity entity) {
TicketSettingsResponse resp = modelMapper.map(entity, TicketSettingsResponse.class);
// 发布版本映射
// if (entity.getBasicSettings() != null) {
// resp.setBasicSettings(
// com.bytedesk.ticket.ticket_settings.sub.dto.TicketBasicSettingsResponse.builder()
// .numberPrefix(entity.getBasicSettings().getNumberPrefix())
// .numberLength(entity.getBasicSettings().getNumberLength())
// .defaultPriority(entity.getBasicSettings().getDefaultPriority())
// .validityDays(entity.getBasicSettings().getValidityDays())
// .autoCloseHours(entity.getBasicSettings().getAutoCloseHours())
// .enableAutoClose(entity.getBasicSettings().getEnableAutoClose())
// .build());
// }
resp.setBasicSettings(mapBasicSettings(entity.getBasicSettings()));
// if (entity.getStatusFlowSettings() != null) {
// resp.setStatusFlowSettings(
// com.bytedesk.ticket.ticket_settings.sub.dto.TicketStatusFlowSettingsResponse.builder()
@@ -989,17 +1034,7 @@ public class TicketSettingsRestService extends
// .build());
// }
// // 草稿版本映射
// if (entity.getDraftBasicSettings() != null) {
// resp.setDraftBasicSettings(
// com.bytedesk.ticket.ticket_settings.sub.dto.TicketBasicSettingsResponse.builder()
// .numberPrefix(entity.getDraftBasicSettings().getNumberPrefix())
// .numberLength(entity.getDraftBasicSettings().getNumberLength())
// .defaultPriority(entity.getDraftBasicSettings().getDefaultPriority())
// .validityDays(entity.getDraftBasicSettings().getValidityDays())
// .autoCloseHours(entity.getDraftBasicSettings().getAutoCloseHours())
// .enableAutoClose(entity.getDraftBasicSettings().getEnableAutoClose())
// .build());
// }
resp.setDraftBasicSettings(mapBasicSettings(entity.getDraftBasicSettings()));
// if (entity.getDraftStatusFlowSettings() != null) {
// resp.setDraftStatusFlowSettings(
// com.bytedesk.ticket.ticket_settings.sub.dto.TicketStatusFlowSettingsResponse.builder()