diff --git a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyCommandServiceImpl.java b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyCommandServiceImpl.java
index f2b32e9..0028730 100644
--- a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyCommandServiceImpl.java
+++ b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyCommandServiceImpl.java
@@ -15,10 +15,15 @@ import com.ruoyi.apt.dispatch.mapper.AptJcxxMapper;
import com.ruoyi.apt.framework.audit.AptAuditWriter;
import com.ruoyi.apt.framework.realtime.IAptRealtimePublisher;
import com.ruoyi.apt.ops.domain.AptOperationLog;
+import com.ruoyi.apt.voice.service.IAptCallFlowService;
import com.ruoyi.common.utils.SecurityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* 受控命令实现(IT-5 / M3,公寓端/04 表 4-9 命令语义)。
@@ -40,6 +45,8 @@ import org.springframework.transaction.annotation.Transactional;
@Service
public class AptDutyCommandServiceImpl implements IAptDutyCommandService
{
+ private static final Logger log = LoggerFactory.getLogger(AptDutyCommandServiceImpl.class);
+
private static final String STOP = "STOP";
private static final String RE_CALL = "RE_CALL";
private static final String FORCE_FINISH = "FORCE_FINISH";
@@ -50,14 +57,17 @@ public class AptDutyCommandServiceImpl implements IAptDutyCommandService
private final IAptDutyStateService stateService;
private final AptAuditWriter auditWriter;
private final IAptRealtimePublisher realtimePublisher;
+ private final IAptCallFlowService callFlowService;
public AptDutyCommandServiceImpl(AptJcxxMapper jcxxMapper, IAptDutyStateService stateService,
- AptAuditWriter auditWriter, IAptRealtimePublisher realtimePublisher)
+ AptAuditWriter auditWriter, IAptRealtimePublisher realtimePublisher,
+ IAptCallFlowService callFlowService)
{
this.jcxxMapper = jcxxMapper;
this.stateService = stateService;
this.auditWriter = auditWriter;
this.realtimePublisher = realtimePublisher;
+ this.callFlowService = callFlowService;
}
@Override
@@ -135,9 +145,11 @@ public class AptDutyCommandServiceImpl implements IAptDutyCommandService
break;
case RE_CALL:
stateService.recall(jcid);
+ scheduleRedialAfterCommit(jcid);
break;
case FORCE_FINISH:
stateService.forceFinish(jcid);
+ scheduleForceHangupAfterCommit(jcid);
break;
case MARK_LEAVE:
String leaveType = command.getLeaveType() == null || command.getLeaveType().isBlank()
@@ -153,6 +165,64 @@ public class AptDutyCommandServiceImpl implements IAptDutyCommandService
}
}
+ /**
+ * RE_CALL 提交后再发起 SIP 重呼:复叫事实(状态迁移)先落库,
+ * 呼叫失败(AMI 未就绪 / Originate 失败)只告警,不回滚已提交的复叫;
+ * 失败后由 30s 超时扫描经 CALL_FAILED 收敛到叫乘报警。
+ */
+ private void scheduleRedialAfterCommit(String jcid)
+ {
+ runAfterCommit(() -> {
+ try
+ {
+ callFlowService.redial(jcid);
+ }
+ catch (Exception e)
+ {
+ log.warn("复叫 {} SIP 重呼发起失败:{}", jcid, e.toString());
+ }
+ });
+ }
+
+ /**
+ * FORCE_FINISH(值班员「结束叫乘」)提交后强制挂断全部在会腿:
+ * 终态(finish_jc=1)先落库,踢人失败只告警;房间腿挂断事件随后到,
+ * 其 ANSWERED 提交因终态已置而被状态机条件更新拦截(STATE_ILLEGAL 忽略)。
+ */
+ private void scheduleForceHangupAfterCommit(String jcid)
+ {
+ runAfterCommit(() -> {
+ try
+ {
+ callFlowService.forceHangupAll(jcid);
+ }
+ catch (Exception e)
+ {
+ log.warn("强制结束 {} 挂断在会腿失败:{}", jcid, e.toString());
+ }
+ });
+ }
+
+ /** 事务提交后执行外部副作用(AMI 调用),避免把外部调用包进数据库事务 */
+ private void runAfterCommit(Runnable task)
+ {
+ if (TransactionSynchronizationManager.isSynchronizationActive())
+ {
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization()
+ {
+ @Override
+ public void afterCommit()
+ {
+ task.run();
+ }
+ });
+ }
+ else
+ {
+ task.run();
+ }
+ }
+
/** 前置状态校验(表 4-9「允许的当前状态」列,逐条对应) */
private void checkAllowed(String action, String ryzt, String finishJc, String finishLy, String stoped)
{
diff --git a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyStateServiceImpl.java b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyStateServiceImpl.java
index 6ad3e62..216d5fa 100644
--- a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyStateServiceImpl.java
+++ b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/dispatch/service/AptDutyStateServiceImpl.java
@@ -7,6 +7,7 @@ import com.ruoyi.apt.common.AptErrorCode;
import com.ruoyi.apt.common.AptException;
import com.ruoyi.apt.dispatch.domain.AptStateSnapshot;
import com.ruoyi.apt.dispatch.mapper.AptDutyStateMapper;
+import com.ruoyi.apt.duty.realtime.DutyTransitionPush;
import com.ruoyi.apt.framework.realtime.IAptRealtimePublisher;
import com.ruoyi.apt.job.service.IAptJobService;
import org.slf4j.Logger;
@@ -26,6 +27,9 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
* 签到窗口超时同样由平板上报 NO_ANSWER(P2-2 裁决);
*
SIP 30s 无入会由扫描任务调用 {@link #callFailed(String)},
* 仍是「提交命令」而非直写。
+ * 每次迁移后统一推值班台(R4 / `L-11`):本类是四类触发源
+ * (人工命令 / Quartz 扫描 / AMI 事件 / 平板上报)的唯一汇合点,
+ * 推送放在这里,定时任务产生的报警才可能到达 `/ws/duty`。
*
*
* @author apt-rev01
@@ -41,13 +45,15 @@ public class AptDutyStateServiceImpl implements IAptDutyStateService
private final AptDutyStateMapper stateMapper;
private final IAptJobService jobService;
private final IAptRealtimePublisher realtimePublisher;
+ private final DutyTransitionPush dutyPush;
public AptDutyStateServiceImpl(AptDutyStateMapper stateMapper, IAptJobService jobService,
- IAptRealtimePublisher realtimePublisher)
+ IAptRealtimePublisher realtimePublisher, DutyTransitionPush dutyPush)
{
this.stateMapper = stateMapper;
this.jobService = jobService;
this.realtimePublisher = realtimePublisher;
+ this.dutyPush = dutyPush;
}
@Override
@@ -60,23 +66,41 @@ public class AptDutyStateServiceImpl implements IAptDutyStateService
@Transactional
public int callStart(String jcid)
{
- return require(stateMapper.updateCallStart(gyid(), jcid, false), jcid, "CALL_START");
+ AptStateSnapshot before = snapshot(jcid);
+ int rows = require(stateMapper.updateCallStart(gyid(), jcid, false), jcid, "CALL_START");
+ pushDuty(jcid, before);
+ return rows;
+ }
+
+ @Override
+ @Transactional
+ public int joinConf(String jcid)
+ {
+ // 房间入会只补记呼叫响应时间:ryzt 仍是 1 叫乘中,值班台无需推送(状态未变);
+ // 待离寓迁移由房间挂断(HANGUP)触发,见 answered()
+ return require(stateMapper.updateJoinConf(gyid(), jcid), jcid, "JOIN_CONF");
}
@Override
@Transactional
public int answered(String jcid)
{
- return require(stateMapper.updateAnswered(gyid(), jcid, ANSWERABLE), jcid, "ANSWERED");
+ AptStateSnapshot before = snapshot(jcid);
+ int rows = require(stateMapper.updateAnswered(gyid(), jcid, ANSWERABLE), jcid, "ANSWERED");
+ pushDuty(jcid, before);
+ return rows;
}
@Override
@Transactional
public int callFailed(String jcid)
{
+ AptStateSnapshot before = snapshot(jcid);
int rows = require(stateMapper.updateCallFailed(gyid(), jcid), jcid, "CALL_FAILED");
- // 无应答 / 超时转叫乘报警:P0,值班台需立即打断
+ // 公寓端通道(/apt/ws):管理台与平板看到叫乘报警
realtimePublisher.alarm(jcid, "CALL_FAILED", "无应答转叫乘报警");
+ // 值班台通道(/ws/duty):本方法也被 Quartz 扫描调用,调度线程没有 HTTP 上下文(L-11)
+ pushDuty(jcid, before);
return rows;
}
@@ -84,19 +108,23 @@ public class AptDutyStateServiceImpl implements IAptDutyStateService
@Transactional
public int stop(String jcid)
{
- return require(stateMapper.updateStop(gyid(), jcid), jcid, "STOP");
+ AptStateSnapshot before = snapshot(jcid);
+ int rows = require(stateMapper.updateStop(gyid(), jcid), jcid, "STOP");
+ pushDuty(jcid, before);
+ return rows;
}
@Override
@Transactional
public int recall(String jcid)
{
+ AptStateSnapshot before = snapshot(jcid);
int rows = require(stateMapper.updateRecall(gyid(), jcid), jcid, "RE_CALL");
- AptStateSnapshot snapshot = stateMapper.selectSnapshot(gyid(), jcid);
- String callMode = snapshot == null || snapshot.getCallMode() == null ? "SIP" : snapshot.getCallMode();
+ String callMode = before == null || before.getCallMode() == null ? "SIP" : before.getCallMode();
// 表 3-10:RE_CALL 生成新会话,保证时间线与录音索引有独立 session_id
stateMapper.insertCallSession(gyid(), "CS-" + UUID.randomUUID().toString().replace("-", ""),
jcid, callMode);
+ pushDuty(jcid, before);
return rows;
}
@@ -104,7 +132,10 @@ public class AptDutyStateServiceImpl implements IAptDutyStateService
@Transactional
public int forceFinish(String jcid)
{
- return require(stateMapper.updateForceFinish(gyid(), jcid), jcid, "FORCE_FINISH");
+ AptStateSnapshot before = snapshot(jcid);
+ int rows = require(stateMapper.updateForceFinish(gyid(), jcid), jcid, "FORCE_FINISH");
+ pushDuty(jcid, before);
+ return rows;
}
/**
@@ -118,10 +149,13 @@ public class AptDutyStateServiceImpl implements IAptDutyStateService
@Transactional
public int markLeave(String jcid, String leaveType, String remark)
{
+ AptStateSnapshot before = snapshot(jcid);
String eventId = "LY-" + UUID.randomUUID().toString().replace("-", "");
int rows = require(stateMapper.updateMarkLeave(gyid(), jcid, leaveType, eventId, remark),
jcid, "MARK_LEAVE");
stateMapper.insertLeaveEvent(gyid(), eventId, jcid, leaveType);
+ // 先推:下面房间号缺失会提前 return,推送不能跟着一起丢
+ pushDuty(jcid, before);
String roomNo = stateMapper.selectRoomNoByJcid(gyid(), jcid);
if (roomNo == null || roomNo.isBlank())
@@ -168,7 +202,22 @@ public class AptDutyStateServiceImpl implements IAptDutyStateService
@Transactional
public int cancel(String jcid)
{
- return require(stateMapper.updateCancel(gyid(), jcid), jcid, "CANCEL");
+ AptStateSnapshot before = snapshot(jcid);
+ int rows = require(stateMapper.updateCancel(gyid(), jcid), jcid, "CANCEL");
+ pushDuty(jcid, before);
+ return rows;
+ }
+
+ /**
+ * 迁移后推送值班台:重读一次快照拿新态,交给 {@link DutyTransitionPush} 判报警起停。
+ *
+ * 推送在事务内登记、提交后发送({@code DutyPushServiceImpl} 的 afterCommit),
+ * 回滚则不发;这里不回填 {@code actionId}——人工命令的幂等键在命令层,
+ * 状态机不知道也不该知道(`04` 表 4-7 的 actionId 仍由命令响应给出)。
+ */
+ private void pushDuty(String jcid, AptStateSnapshot before)
+ {
+ dutyPush.afterTransition(jcid, before, snapshot(jcid), null);
}
private int require(int rows, String jcid, String transition)
diff --git a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java
index ad1c88a..25a74d8 100644
--- a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java
+++ b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java
@@ -468,6 +468,8 @@ public class AptProperties
private java.util.List dutyExtensions = new java.util.ArrayList<>();
/** 单次扫描最多发起的 SIP 呼叫数,避免补跑时瞬间打爆 Asterisk */
private int maxCallsPerScan = 10;
+ /** 会议开始后自动挂机秒数:值班员未主动挂断时到点强制挂断全部在会腿;<=0 关闭 */
+ private int autoHangupSeconds = 45;
public boolean isEnabled()
{
@@ -569,6 +571,16 @@ public class AptProperties
this.dutyExtensions = dutyExtensions;
}
+ public int getAutoHangupSeconds()
+ {
+ return autoHangupSeconds;
+ }
+
+ public void setAutoHangupSeconds(int autoHangupSeconds)
+ {
+ this.autoHangupSeconds = autoHangupSeconds;
+ }
+
public int getMaxCallsPerScan()
{
return maxCallsPerScan;
diff --git a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/mapper/AptVoiceMapper.java b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/mapper/AptVoiceMapper.java
index 38ad9c9..5fe6187 100644
--- a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/mapper/AptVoiceMapper.java
+++ b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/mapper/AptVoiceMapper.java
@@ -57,6 +57,10 @@ public interface AptVoiceMapper
int markSessionsUnknown(@Param("gyid") String gyid, @Param("amiSessionId") String amiSessionId,
@Param("cause") String cause);
+ /** 按 jcid 取在会(未结束)的呼叫腿:自动挂机与强制结束共用 */
+ java.util.List> selectActiveSessionsByJcid(
+ @Param("gyid") String gyid, @Param("jcid") String jcid);
+
Map selectCallSession(@Param("gyid") String gyid,
@Param("sessionId") String sessionId);
diff --git a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptAmiEventHandler.java b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptAmiEventHandler.java
index 1c3292b..36c3cd7 100644
--- a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptAmiEventHandler.java
+++ b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptAmiEventHandler.java
@@ -21,7 +21,8 @@ import org.springframework.stereotype.Service;
* 房间腿的 {@code ConfbridgeJoin} 才算应答(提交 {@code ANSWERED}),
* 值班台腿入会只记会话状态;
* {@code OriginateResponse.Failure} 只有房间腿失败才提交 {@code CALL_FAILED};
- * {@code Hangup} 只归档会话,不回写叫乘终态;
+ * 房间腿 {@code Hangup}(HANGUP)提交 {@code ANSWERED}——房间挂断即通话完成,
+ * 转待离寓;值班台/TTS 腿挂断只归档会话;
* AMI 断连 → 在途会话标 {@code UNKNOWN} + 立即触发重连,不推定叫乘结论。
*
*
@@ -180,7 +181,10 @@ public class AptAmiEventHandler
markTtsPlayed(gyid, session);
}
voiceMapper.updateCallSessionState(gyid, sessionId, "HANGUP", false, true, cause);
- record(gyid, session, LEG_ROOM.equals(legRole) ? "HANGUP" : "DUTY_HANGUP", frame);
+ // 事件类型按腿角色区分:值班台挂断(DUTY_HANGUP)由事件服务提交 ANSWERED,
+ // 房间/TTS 挂断只归档,绝不推定叫乘结论
+ record(gyid, session, LEG_ROOM.equals(legRole) ? "HANGUP"
+ : LEG_TTS.equals(legRole) ? "TTS_HANGUP" : "DUTY_HANGUP", frame);
}
/** AMI 断连:在途会话标 UNKNOWN,并通知连接管理器立刻重连 */
diff --git a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptCallFlowServiceImpl.java b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptCallFlowServiceImpl.java
index bdfc7c1..b3a0471 100644
--- a/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptCallFlowServiceImpl.java
+++ b/dev/RuoYi-Vue-master/ruoyi-apt/src/main/java/com/ruoyi/apt/voice/service/AptCallFlowServiceImpl.java
@@ -6,6 +6,9 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
import com.ruoyi.apt.common.AptContext;
import com.ruoyi.apt.common.AptErrorCode;
import com.ruoyi.apt.common.AptException;
@@ -18,6 +21,7 @@ import com.ruoyi.apt.voice.ami.AmiConnectionManager;
import com.ruoyi.apt.voice.ami.AmiException;
import com.ruoyi.apt.voice.domain.AptAmiInstance;
import com.ruoyi.apt.voice.mapper.AptVoiceMapper;
+import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -28,13 +32,17 @@ import org.springframework.stereotype.Service;
* 调用顺序(缺一不可):
*
* 校验 ryzt=0 → 解析房间分机 → CALL_START(0→1) → 建腿(apt_call_session)
- * → AMI Originate(房间 + 值班台)→ 等 OriginateResponse 绑定真实通道
+ * → AMI Originate(值班台进 ,房间进 -admin)→ 等 OriginateResponse 绑定真实通道
* → ConfbridgeJoin(房间腿) → ANSWERED(1→3) → TTS 播报
*
*
* 失败语义:房间腿 Originate 失败 → {@code CALL_FAILED}(ryzt=2);
* 值班台腿失败只告警,不影响房间侧结论;TTS 失败只告警,不改叫乘结论。
*
+ * 会议收尾:值班员未主动挂断时,会议开始 {@code auto-hangup-seconds}(默认 45s)后
+ * 由本类定时器强制挂断全部在会腿;值班员「结束叫乘」(FORCE_FINISH)同样挂断全部在会腿。
+ * 房间腿挂断事件提交 ANSWERED → 待离寓。
+ *
* @author apt-rev01
*/
@Service
@@ -52,6 +60,14 @@ public class AptCallFlowServiceImpl implements IAptCallFlowService
private final AmiConnectionManager connectionManager;
private final AptProperties properties;
+ /** 会议自动挂机定时器(单线程守护;Quartz 当前未启用,与连接心跳同一理由自建) */
+ private final ScheduledExecutorService autoHangupScheduler =
+ Executors.newSingleThreadScheduledExecutor(runnable -> {
+ Thread thread = new Thread(runnable, "apt-voice-auto-hangup");
+ thread.setDaemon(true);
+ return thread;
+ });
+
public AptCallFlowServiceImpl(AptVoiceMapper voiceMapper, IAptDutyStateService stateService,
IAptVoiceEventService voiceEventService,
AmiConnectionManager connectionManager, AptProperties properties)
@@ -92,20 +108,78 @@ public class AptCallFlowServiceImpl implements IAptCallFlowService
throw new AptException(AptErrorCode.STATE_ILLEGAL, "CALL_ALREADY_STARTED",
"仅 ryzt=0 且未停止的叫乘可发起呼叫,当前 ryzt=" + snapshot.getRyzt());
}
+ AmiClient ami = requireAmi();
+ // gy_jcxx.fjh 是房号(room_no),不是 room_id;换算与关联统一在 Mapper 内完成
+ String roomNo = str(jcxx.get("fjh"));
+ Map roomEndpoint = requireRoomEndpoint(gyid, roomNo);
+
+ // 先落状态机再发起呼叫:即使 Originate 立即失败,也会经 CALL_FAILED 收敛到 ryzt=2
+ stateService.callStart(jcid);
+
+ return originateLegs(ami, connectionManager.currentInstance(), gyid, jcid, roomNo, roomEndpoint);
+ }
+
+ /**
+ * 复叫重呼(RE_CALL)。
+ *
+ * 与首呼的区别:状态机已在 {@code recall} 中完成迁移(ryzt=1、清 stoped、新会话),
+ * 这里不再走 CALL_START,直接重建房间腿 + 值班台腿;房间腿 Originate 失败仍经
+ * CALL_FAILED 收敛到 ryzt=2(复叫 30s 无入会语义由 jcsj_s 重置后重新起算)。
+ */
+ @Override
+ public Map redial(String jcid)
+ {
+ String gyid = gyid();
+ Map jcxx = voiceMapper.selectJcxxForCall(gyid, jcid);
+ if (jcxx == null)
+ {
+ throw new AptException(AptErrorCode.NOT_FOUND, "JCXX_NOT_FOUND", "叫乘记录不存在:" + jcid);
+ }
+ if (!"SIP".equals(str(jcxx.get("callMode"))))
+ {
+ throw new AptException(AptErrorCode.BAD_REQUEST, "CALL_MODE_NOT_SIP",
+ "该叫乘为平板链路,不可走 SIP 呼叫");
+ }
+ if ("1".equals(str(jcxx.get("finishJc"))) || "1".equals(str(jcxx.get("finishLy"))))
+ {
+ throw new AptException(AptErrorCode.STATE_ILLEGAL, "CALL_ALREADY_FINISHED",
+ "该叫乘已完成或已离寓,不可再呼叫");
+ }
+ AptStateSnapshot snapshot = stateService.snapshot(jcid);
+ if (snapshot == null)
+ {
+ throw new AptException(AptErrorCode.NOT_FOUND, "JCXX_NOT_FOUND", "叫乘记录不存在:" + jcid);
+ }
+ if (!"1".equals(snapshot.getRyzt()) || "1".equals(snapshot.getStoped()))
+ {
+ throw new AptException(AptErrorCode.STATE_ILLEGAL, "RECALL_STATE_ILLEGAL",
+ "仅复叫后(ryzt=1 且未停止)的叫乘可重呼,当前 ryzt=" + snapshot.getRyzt());
+ }
+ AmiClient ami = requireAmi();
+ String roomNo = str(jcxx.get("fjh"));
+ Map roomEndpoint = requireRoomEndpoint(gyid, roomNo);
+ return originateLegs(ami, connectionManager.currentInstance(), gyid, jcid, roomNo, roomEndpoint);
+ }
+
+ /** AMI 连接与实例校验,任一缺失即抛依赖不可用 */
+ private AmiClient requireAmi()
+ {
if (!connectionManager.isUp())
{
throw new AptException(AptErrorCode.DEPENDENCY_UNAVAILABLE, "AMI_NOT_CONNECTED",
"AMI 连接未就绪,无法发起语音叫乘");
}
AmiClient ami = connectionManager.current();
- AptAmiInstance instance = connectionManager.currentInstance();
- if (instance == null)
+ if (ami == null || connectionManager.currentInstance() == null)
{
throw new AptException(AptErrorCode.DEPENDENCY_UNAVAILABLE, "AMI_INSTANCE_MISSING",
"公寓未配置 Asterisk 实例");
}
- // gy_jcxx.fjh 是房号(room_no),不是 room_id;换算与关联统一在 Mapper 内完成
- String roomNo = str(jcxx.get("fjh"));
+ return ami;
+ }
+
+ private Map requireRoomEndpoint(String gyid, String roomNo)
+ {
Map roomEndpoint = voiceMapper.selectRoomExtension(gyid, roomNo);
if (roomEndpoint == null)
{
@@ -113,23 +187,25 @@ public class AptCallFlowServiceImpl implements IAptCallFlowService
"房间 " + roomNo + " 未绑定可用分机(apt_master_room.room_no → "
+ "apt_sip_binding.room_id → apt_sip_endpoint)");
}
+ return roomEndpoint;
+ }
- // 先落状态机再发起呼叫:即使 Originate 立即失败,也会经 CALL_FAILED 收敛到 ryzt=2
- stateService.callStart(jcid);
-
+ /**
+ * 值班台腿 + 房间腿 Originate(先值班台,后房间)。
+ *
+ * 值班台腿邀请进 {@code }(如 Conf-01,值班员接通前提示音由
+ * Asterisk 拨号计划负责),房间腿进 {@code -admin}(如 Conf-01-admin)。
+ * 房间腿失败即 CALL_FAILED 并抛依赖不可用;值班台腿失败只告警,不影响房间侧结论。
+ * 双腿发起成功后登记会议开始,到点未收尾由自动挂机兜底。
+ */
+ private Map originateLegs(AmiClient ami, AptAmiInstance instance, String gyid,
+ String jcid, String roomNo, Map roomEndpoint)
+ {
String confNo = instance.getConference();
+ String roomConfNo = confNo + "-admin";
List