add dial group/queue

This commit is contained in:
东皇大叔 2020-07-10 10:00:11 +08:00
parent 2e4bd458f2
commit 24d1881fa0
57 changed files with 556 additions and 697 deletions

View File

@ -0,0 +1,35 @@
package com.pudonghot.yo.cms.controller;
import com.wacai.tigon.form.FormList;
import com.pudonghot.yo.model.domain.Agent;
import com.wacai.tigon.web.annotation.ListApi;
import com.wacai.tigon.web.annotation.FilterCol;
import com.pudonghot.yo.model.domain.AgentStatus;
import org.springframework.stereotype.Controller;
import com.pudonghot.yo.cms.annotation.TenantResource;
import com.wacai.tigon.web.controller.BaseQueryController;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Donghuang
* @date Jul 09, 2020 16:44:23
*/
@Controller
@ListApi(searchCols = {
Agent.NAME,
Agent.ACCOUNT,
Agent.AGENT,
Agent.NOTE
},
filterCols = {
@FilterCol(param = AgentStatus.STATUS, type = AgentStatus.Status.class),
@FilterCol(param = AgentStatus.STATE, type = AgentStatus.State.class),
@FilterCol(param = AgentStatus.REGISTERED, type = boolean.class),
})
@TenantResource
@RequestMapping("/agent-status")
public class AgentStatusController
extends BaseQueryController<Integer,
AgentStatus,
FormList> {
}

View File

@ -2,10 +2,7 @@ package com.pudonghot.yo.cms.form.create;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.*;
import com.wacai.tigon.format.annotation.Trim;
import com.pudonghot.yo.cms.form.BaseCreateForm;
@ -20,10 +17,14 @@ public class CreateFormSequence extends BaseCreateForm {
@NotBlank
@Pattern(regexp = "^\\w+$", message = "序列名必须是字母数字下划线构成")
private String name;
@Min(2)
@Max(16)
private int length;
@Min(1)
@NotNull
private Long initVal;
@Min(1)
@Max(128)
@NotNull
private Long step;
}

View File

@ -0,0 +1,12 @@
package com.pudonghot.yo.cms.service;
import com.pudonghot.yo.model.domain.AgentStatus;
import com.wacai.tigon.service.BaseQueryService;
/**
* @author Donghuang
* @date Jul 09, 2020 16:53:30
*/
public interface AgentStatusService
extends BaseQueryService<Integer, AgentStatus> {
}

View File

@ -1,9 +1,9 @@
package com.pudonghot.yo.cms.service;
import com.pudonghot.yo.cms.form.create.CreateFormSequence;
import com.pudonghot.yo.cms.form.update.UpdateFormSequence;
import com.pudonghot.yo.model.domain.Sequence;
import com.wacai.tigon.service.BaseCrudByFormService;
import com.pudonghot.yo.cms.form.create.CreateFormSequence;
import com.pudonghot.yo.cms.form.update.UpdateFormSequence;
/**
* @author Donghuang <br>
@ -22,4 +22,13 @@ public interface SequenceService
* @return next seq val
*/
Long nextVal(Integer tenantId, String name);
/**
* next seq val string, padding 0 to length
*
* @param tenantId tenant id
* @param name seq name
* @return next val
*/
String nextValStr(Integer tenantId, String name);
}

View File

@ -99,8 +99,7 @@ public class AgentGroupServiceImpl
super.beforeInsert(model);
if (StringUtils.isBlank(model.getIdentifier())) {
model.setIdentifier(identifierPrefix +
StringUtils.leftPad(String.valueOf(
seqService.nextVal(model.getTenantId(), seqName)), 6, '0'));
seqService.nextValStr(model.getTenantId(), seqName));
}
}

View File

@ -131,7 +131,7 @@ public class AgentServiceImpl
protected void beforeInsert(final Agent model) {
super.beforeInsert(model);
if (StringUtils.isBlank(model.getAgent())) {
model.setAgent(String.valueOf(seqService.nextVal(model.getTenantId(), seqName)));
model.setAgent(seqService.nextValStr(model.getTenantId(), seqName));
}
if (StringUtils.isBlank(model.getPassword())) {

View File

@ -0,0 +1,19 @@
package com.pudonghot.yo.cms.service.impl;
import org.springframework.stereotype.Service;
import com.pudonghot.yo.mapper.AgentStatusMapper;
import com.pudonghot.yo.model.domain.AgentStatus;
import com.pudonghot.yo.cms.service.AgentStatusService;
import com.wacai.tigon.service.support.BaseQueryServiceSupport;
/**
* @author Donghuang <br>
* Dec 24, 2019 16:49:51
*/
@Service
public class AgentStatusServiceImpl
extends BaseQueryServiceSupport<Integer,
AgentStatus,
AgentStatusMapper>
implements AgentStatusService {
}

View File

@ -50,9 +50,7 @@ public class QueueServiceImpl
final Tenant tenant = tenantMapper.find(tenantId);
Assert.state(tenant != null,
() -> "No tenant [" + tenantId + "] found");
model.setIdentifier(identifierPrefix +
StringUtils.leftPad(String.valueOf(
seqService.nextVal(tenantId, seqName)), 6, '0'));
model.setIdentifier(identifierPrefix + seqService.nextValStr(tenantId, seqName));
}
}
}

View File

@ -1,13 +1,14 @@
package com.pudonghot.yo.cms.service.impl;
import com.pudonghot.yo.cms.form.create.CreateFormSequence;
import lombok.extern.slf4j.Slf4j;
import com.wacai.tigon.mybatis.Search;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import org.springframework.stereotype.Service;
import com.pudonghot.yo.mapper.SequenceMapper;
import com.pudonghot.yo.model.domain.Sequence;
import com.pudonghot.yo.cms.service.SequenceService;
import com.pudonghot.yo.cms.form.create.CreateFormSequence;
import com.pudonghot.yo.cms.form.update.UpdateFormSequence;
import org.springframework.transaction.annotation.Transactional;
import com.wacai.tigon.service.support.BaseCrudByFormServiceSupport;
@ -42,17 +43,18 @@ public class SequenceServiceImpl
* {@inheritDoc}
*/
@Override
@Transactional
@Transactional(rollbackFor = RuntimeException.class)
public Long nextVal(final Integer tenantId, final String name) {
final Sequence seq = mapper.findForUpdate(
new Search(Sequence.NAME, name)
.eq(Sequence.TENANT_ID, tenantId)
.eq(Sequence.ACTIVE, true));
Assert.state(seq != null, () -> "No valid sequence [" + name + "] found");
final Long nextVal = seq.getCurrentVal() + seq.getStep();
seq.setCurrentVal(nextVal);
mapper.update(seq);
return nextVal;
return nextVal(tenantId, name, false);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = RuntimeException.class)
public String nextValStr(final Integer tenantId, final String name) {
return nextVal(tenantId, name, true);
}
/**
@ -61,8 +63,28 @@ public class SequenceServiceImpl
@Override
protected void beforeInsert(final Sequence model) {
super.beforeInsert(model);
Assert.state(String.valueOf(model.getInitVal()).length() <= model.getLength(),
"Sequence init value exceeds length");
if (model.getCurrentVal() == null) {
model.setCurrentVal(model.getInitVal());
}
}
<T> T nextVal(final Integer tenantId, final String name, final boolean str) {
final Sequence seq = mapper.findForUpdate(
new Search(Sequence.TENANT_ID, tenantId)
.eq(Sequence.NAME, name)
.eq(Sequence.ACTIVE, true));
Assert.state(seq != null, () ->
"No valid sequence [" + name + "] found");
final Long nextVal = seq.getCurrentVal() + seq.getStep();
final String strVal = String.valueOf(nextVal);
Assert.state(strVal.length() <= seq.getLength(),
"Sequence next value exceeds length");
seq.setCurrentVal(nextVal);
mapper.update(seq);
return str ? (T) StringUtils.leftPad(strVal, seq.getLength(), '0')
: (T) nextVal;
}
}

View File

@ -1,29 +1,27 @@
package com.pudonghot.yo.cms.service.impl;
import java.util.*;
import com.pudonghot.yo.cms.form.SessionForm;
import com.pudonghot.yo.cms.form.create.CreateFormTrunk;
import com.pudonghot.yo.mapper.*;
import com.pudonghot.yo.model.domain.*;
import lombok.extern.slf4j.Slf4j;
import java.util.stream.Collectors;
import java.util.function.BiConsumer;
import com.wacai.tigon.mybatis.Search;
import com.pudonghot.yo.model.domain.*;
import com.wacai.tigon.model.ViewModel;
import org.springframework.util.Assert;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.StringUtils;
import com.pudonghot.yo.cms.form.SessionForm;
import org.springframework.stereotype.Service;
import com.pudonghot.yo.cellphone.CellphoneInfo;
import com.pudonghot.yo.common.util.MobileUtils;
import com.pudonghot.yo.cellphone.CellphoneService;
import com.pudonghot.yo.cms.service.TrunkService;
import com.pudonghot.yo.cellphone.CellphoneService;
import com.pudonghot.yo.cms.service.AreaCodeService;
import com.pudonghot.yo.cms.service.SequenceService;
import org.springframework.beans.factory.annotation.Value;
import com.pudonghot.yo.cms.form.create.CreateFormTrunk;
import com.pudonghot.yo.cms.form.update.UpdateFormTrunk;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
/**
@ -142,8 +140,7 @@ public class TrunkServiceImpl
protected void beforeInsert(final Trunk model) {
super.beforeInsert(model);
if (StringUtils.isBlank(model.getPrefix())) {
model.setPrefix(String.valueOf(
seqService.nextVal(model.getTenantId(), seqName)));
model.setPrefix(seqService.nextValStr(model.getTenantId(), seqName));
}
}

View File

@ -24,7 +24,8 @@ spring.redis.port=6379
# CAS
tigon.shiro.cas.server.addr=http://118.24.251.131:8080/cas
tigon.shiro.cas.client.addr=http://118.24.251.131:8080/cms/
tigon.shiro.cas.client.addr=http://118.24.251.131:8080/cms
tigon.shiro.cas.login-success-url=/cms/
tigon.shiro.filter-chain=${site.context-path}/auth/login=anon \
/=anon \
/state-machine/**=anon \

View File

@ -1,8 +1,8 @@
package com.pudonghot.yo.fsagent.controller;
import com.pudonghot.yo.model.domain.Tenant;
import lombok.extern.slf4j.Slf4j;
import com.wacai.tigon.mybatis.Search;
import com.pudonghot.yo.model.domain.Tenant;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
@ -43,4 +43,12 @@ public class BaseDialplanController extends BaseXmlController {
thisDn + "-" +
otherDn + recordingFileExt;
}
/**
* return empty dial plan
* @return empty dial plan
*/
protected FreeMarkerView empty() {
return empty(SECTION_DIALPLAN);
}
}

View File

@ -5,7 +5,6 @@ import java.util.List;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import com.wacai.tigon.mybatis.Search;
import org.apache.commons.lang3.StringUtils;
import com.pudonghot.yo.model.domain.Gateway;
import com.alibaba.druid.pool.DruidDataSource;
import com.pudonghot.yo.fsagent.util.OdbcUtils;
@ -55,8 +54,7 @@ public class ConfigController extends BaseXmlController {
final FreeMarkerView view = view("config/sofia.conf.xml");
final List<Gateway> gateways = gatewayService.list(
new Search(Gateway.ACTIVE, true));
gateways.forEach(g -> g.setName(
"GW" + StringUtils.leftPad(String.valueOf(g.getId()), 6, '0')));
gateways.forEach(g -> g.setName(gatewayService.genGatewayName(g)));
attr(view, "gateways", gateways);
return view;

View File

@ -2,14 +2,20 @@ package com.pudonghot.yo.fsagent.controller;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import com.pudonghot.yo.fsagent.service.*;
import com.pudonghot.yo.model.domain.Queue;
import com.pudonghot.yo.model.domain.Trunk;
import com.pudonghot.yo.model.domain.Agent;
import com.wacai.tigon.sequence.IdSequence;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.util.DigestUtils;
import com.pudonghot.yo.model.domain.AgentGroup;
import org.springframework.stereotype.Controller;
import com.pudonghot.yo.fsagent.service.AgentService;
import com.pudonghot.yo.fsagent.service.LocalApiService;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import static com.pudonghot.yo.fsagent.util.CallStrUtils.DialTarget;
import org.springframework.web.servlet.view.freemarker.FreeMarkerView;
/**
@ -22,9 +28,17 @@ public class DialplanInternalController extends BaseDialplanController {
@Autowired
private AgentService agentService;
@Autowired
private AgentGroupService agentGroupService;
@Autowired
private QueueService queueService;
@Autowired
private LocalApiService localApiService;
@Autowired
private IdSequence idSeq;
@Autowired
private TrunkService trunkService;
@Autowired
private GatewayService gatewayService;
@RequestMapping(params = {
"variable_domain_name",
@ -41,20 +55,102 @@ public class DialplanInternalController extends BaseDialplanController {
@RequestParam
final Map<String, String> params) {
log.info("XML dialplan of domain [{}].", domain);
log.info("XML dialplan of domain [{}] [{}] -> [{}].", domain, user, calledNumber);
log.debug("XML dialplan params [{}].", params);
final Agent callerAgent =
agentService.findByDomainAndAgent(domain, user);
final DialTarget dialTarget =
CallStrUtils.parseDialTarget(calledNumber);
if (dialTarget != null) {
final Integer dialTargetId = dialTarget.getId();
final DialTarget.Type dialTargetType = dialTarget.getType();
if (dialTargetType == DialTarget.Type.AGENT) {
final Agent calledAgent = agentService.find(dialTargetId);
if (calledAgent != null) {
return agentCallAgent(domain, callerAgent, calledAgent);
}
log.warn("No agent found of dial target [{}].", calledNumber);
return empty();
}
else if (dialTargetType == DialTarget.Type.AGENT_GROUP) {
final AgentGroup agentGroup = agentGroupService.find(dialTargetId);
if (agentGroup != null) {
final Agent calledAgent =
agentService.lockIdleOfGroup(dialTargetId);
if (calledAgent != null) {
return agentCallAgent(domain, callerAgent, calledAgent);
}
log.warn("No idle agent found of group [{}].", calledNumber);
return empty();
}
log.warn("No agent group found of dial target [{}].", calledNumber);
return empty();
}
else if (dialTargetType == DialTarget.Type.QUEUE) {
final Queue queue = queueService.find(dialTargetId);
if (queue != null) {
final Agent calledAgent =
agentService.lockIdleOfQueue(dialTargetId);
if (calledAgent != null) {
return agentCallAgent(domain, callerAgent, calledAgent);
}
log.warn("No idle agent found of queue [{}].", calledNumber);
return empty();
}
log.warn("No queue found of dial target [{}].", calledNumber);
return empty();
}
log.error("Never here!!!");
}
final Pair<Trunk, String> trunkInfo = trunkService.parseDialTarget(
callerAgent.getTenantId(), calledNumber);
if (trunkInfo != null) {
log.info("Trunk outbound call found.");
final Trunk trunk = trunkInfo.getLeft();
final String targetNumber = trunkInfo.getRight();
final FreeMarkerView view = view(domain, "trunk-outbound.xml");
final String connId = idSeq.get();
attr(view, "connId", connId);
attr(view, "callerAgent", callerAgent);
attr(view, "gatewayName",
gatewayService.genGatewayName(trunk.getGatewayId()));
attr(view, "targetNumber", targetNumber);
attr(view, "recordingLoc", recordingFile(connId,
callerAgent.getAccount(),
DigestUtils.md5DigestAsHex(targetNumber.getBytes())));
attr(view, "postRecUrl", localApiService.getPostRecUrl());
return view;
}
final Agent calledAgent =
agentService.findOfCalled(domain, calledNumber);
if (calledAgent != null) {
log.info("Local extension [{}] found.", calledAgent);
return agentCallAgent(domain, callerAgent, calledAgent);
}
log.warn("No correct dialplan found for called number [{}].", calledNumber);
return empty();
}
private FreeMarkerView agentCallAgent(final String domain, Agent callerAgent, Agent calledAgent) {
final FreeMarkerView view = view(domain, "local-extension.xml");
final String connId = idSeq.get();
attr(view, "connId", connId);
attr(view, "destinationUser", calledAgent.getAgent());
final Agent callerAgent =
agentService.findByDomainAndAgent(domain, user);
attr(view, "callerAgent", callerAgent);
attr(view, "calledAgent", calledAgent);
attr(view, "recordingLoc", recordingFile(connId,
@ -63,9 +159,4 @@ public class DialplanInternalController extends BaseDialplanController {
attr(view, "postRecUrl", localApiService.getPostRecUrl());
return view;
}
// TODO Dial Queue
return view(domain, "dialplan.xml");
}
}

View File

@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
import com.pudonghot.yo.fsagent.util.EslEventUtils;
import com.pudonghot.yo.model.agentevent.AgentEvent;
import com.pudonghot.yo.fsagent.service.AgentService;
import com.pudonghot.yo.fsagent.util.ChannelNameUtils;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
import org.freeswitch.esl.client.transport.event.Event;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
@ -39,13 +39,8 @@ public class ChannelAnswer {
public void onChannelAnswer(final Event event) {
log.debug("On channel answer event [{}] [{}].", event, event.getHeaders());
// sofia/internal/700001@d1.wacai.info
// sofia/external/013764268709
final String channelName = event.getHeader("variable_channel_name");
log.info("On channel [{}] answer event.", channelName);
final ChannelNameUtils.ChannelInfo channelInfo =
ChannelNameUtils.parse(channelName);
final CallStrUtils.ChannelInfo channelInfo =
CallStrUtils.getChannelInfo(event);
if (channelInfo.isAgent()) {
final Agent agent = findAgent(channelInfo);
@ -81,7 +76,7 @@ public class ChannelAnswer {
}
}
private Agent findAgent(final ChannelNameUtils.ChannelInfo channelInfo) {
private Agent findAgent(final CallStrUtils.ChannelInfo channelInfo) {
return agentService.findByDomainAndAgent(
channelInfo.getDomain(), channelInfo.getNumber());
}

View File

@ -2,7 +2,7 @@ package com.pudonghot.yo.fsagent.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import com.pudonghot.yo.fsagent.util.ChannelNameUtils;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
import org.freeswitch.esl.client.transport.event.Event;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
@ -29,7 +29,7 @@ public class ChannelCreate {
public void onChannelCreate(final Event event) {
log.debug("On channel create event [{}] [{}].", event, event.getHeaders());
final ChannelNameUtils.ChannelInfo channelInfo = ChannelNameUtils.get(event);
final CallStrUtils.ChannelInfo channelInfo = CallStrUtils.getChannelInfo(event);
if (channelInfo.isAgent()) {
agentStatusService.inACall(channelInfo.getDomain(),

View File

@ -9,14 +9,14 @@ import org.springframework.stereotype.Component;
import com.pudonghot.yo.fsagent.util.EslEventUtils;
import com.pudonghot.yo.model.agentevent.AgentEvent;
import com.pudonghot.yo.fsagent.service.AgentService;
import com.pudonghot.yo.fsagent.util.ChannelNameUtils;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.freeswitch.esl.client.transport.event.Event;
import com.pudonghot.yo.service.CommonAgentStatusService;
import com.pudonghot.yo.service.CommonAgentEventQueueService;
import org.springframework.beans.factory.annotation.Autowired;
import com.pudonghot.yo.fsagent.util.ChannelNameUtils.ChannelInfo;
import com.pudonghot.yo.fsagent.util.CallStrUtils.ChannelInfo;
import static com.pudonghot.yo.model.agentevent.EventType.AgentOther_PhoneRelease;
import static com.pudonghot.yo.model.agentevent.EventType.AgentEvent_Customer_Release;
@ -47,7 +47,7 @@ public class ChannelDestroy {
final String presenceId = event.getHeader("variable_presence_id");
final String callUuid = event.getCallUuid();
log.info("On channel [{}][{}] destroy event.", presenceId, callUuid);
final ChannelInfo channelInfo = ChannelNameUtils.get(event);
final ChannelInfo channelInfo = CallStrUtils.getChannelInfo(event);
if (channelInfo.isAgent()) {
agentStatusService.acw(channelInfo.getDomain(),

View File

@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
import com.pudonghot.yo.fsagent.util.EslEventUtils;
import com.pudonghot.yo.fsagent.service.AgentService;
import com.pudonghot.yo.model.agentevent.AgentEvent;
import com.pudonghot.yo.fsagent.util.ChannelNameUtils;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
import org.freeswitch.esl.client.transport.event.Event;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
@ -41,8 +41,8 @@ public class ChannelHangupComplete {
if (!event.isHangupNormalClearing()) {
final ChannelNameUtils.ChannelInfo channelInfo =
ChannelNameUtils.get(event);
final CallStrUtils.ChannelInfo channelInfo =
CallStrUtils.getChannelInfo(event);
if (channelInfo.isAgent()) {
final Agent agent = agentService.findByDomainAndAgent(

View File

@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
import com.pudonghot.yo.fsagent.util.EslEventUtils;
import com.pudonghot.yo.model.agentevent.AgentEvent;
import com.pudonghot.yo.fsagent.service.AgentService;
import com.pudonghot.yo.fsagent.util.ChannelNameUtils;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
import org.freeswitch.esl.client.transport.event.Event;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
@ -42,8 +42,8 @@ public class ChannelProgress {
log.info("On channel [{}] answer event.", channelName);
if (EslEventUtils.isCalled(event)) {
final ChannelNameUtils.ChannelInfo channelInfo =
ChannelNameUtils.get(event);
final CallStrUtils.ChannelInfo channelInfo =
CallStrUtils.getChannelInfo(event);
if (channelInfo.isAgent()) {
final Agent agent = agentService.findByDomainAndAgent(
channelInfo.getDomain(),

View File

@ -8,4 +8,20 @@ import com.wacai.tigon.service.BaseQueryService;
* Dec 04, 2019 19:04:12
*/
public interface GatewayService extends BaseQueryService<Integer, Gateway> {
/**
* generate gateway name
*
* @param gateway gateway
* @return gateway name
*/
String genGatewayName(Gateway gateway);
/**
* generate gateway name
*
* @param gatewayId gatewayId
* @return gateway name
*/
String genGatewayName(Integer gatewayId);
}

View File

@ -27,4 +27,13 @@ public interface TrunkService extends BaseQueryService<Integer, Trunk> {
* @return called number
*/
String calledNumber(Trunk trunk, String number);
/**
* parse dial target
*
* @param tenantId tenant id
* @param calledNumber called number
* @return trunk and called number
*/
Pair<Trunk, String> parseDialTarget(Integer tenantId, String calledNumber);
}

View File

@ -1,10 +1,11 @@
package com.pudonghot.yo.fsagent.service.impl;
import com.pudonghot.yo.fsagent.service.GatewayService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import com.pudonghot.yo.mapper.GatewayMapper;
import com.pudonghot.yo.model.domain.Gateway;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.pudonghot.yo.fsagent.service.GatewayService;
import com.wacai.tigon.service.support.BaseQueryServiceSupport;
/**
@ -18,4 +19,20 @@ public class GatewayerviceImpl
Gateway,
GatewayMapper>
implements GatewayService {
/**
* {@inheritDoc}
*/
@Override
public String genGatewayName(final Gateway gateway) {
return genGatewayName(gateway.getId());
}
/**
* {@inheritDoc}
*/
@Override
public String genGatewayName(Integer gatewayId) {
return "GW" + StringUtils.leftPad(String.valueOf(gatewayId), 6, '0');
}
}

View File

@ -2,14 +2,19 @@ package com.pudonghot.yo.fsagent.service.impl;
import java.util.List;
import com.pudonghot.yo.fsagent.service.GatewayService;
import com.pudonghot.yo.fsagent.service.TrunkService;
import com.pudonghot.yo.mapper.GatewayMapper;
import com.pudonghot.yo.mapper.SequenceMapper;
import com.pudonghot.yo.mapper.TrunkMapper;
import com.pudonghot.yo.mapper.TrunkStrategyMapper;
import com.pudonghot.yo.model.domain.Gateway;
import com.pudonghot.yo.model.domain.Sequence;
import com.pudonghot.yo.model.domain.Trunk;
import com.pudonghot.yo.model.domain.TrunkStrategy;
import com.wacai.tigon.mybatis.Search;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Assert;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.StringUtils;
@ -32,12 +37,18 @@ public class TrunkServiceImpl
TrunkMapper>
implements TrunkService {
@Value("${yo.fsagent.trunk-prefix.seq-name:TRUNK}")
private String seqName;
@Autowired
private SequenceMapper sequenceMapper;
@Autowired
private TrunkStrategyMapper trunkStrategyMapper;
@Autowired
private GatewayMapper gatewayMapper;
@Autowired
private CellphoneService cellphoneService;
@Autowired
private GatewayService gatewayService;
/**
* {@inheritDoc}
@ -70,8 +81,8 @@ public class TrunkServiceImpl
Assert.state(gateway.getActive(),
() -> "Trunk [" + trunk.getPrefix() + "] gateway is not active");
return Pair.of(trunk, "sofia/gateway/GW" +
StringUtils.leftPad(String.valueOf(gateway.getId()), 6, '0') +
return Pair.of(trunk, "sofia/gateway/" +
gatewayService.genGatewayName(gateway) +
"/" + calledNumber(trunk, number));
}
@ -100,6 +111,30 @@ public class TrunkServiceImpl
return addPrefix(trunk, calledNumber);
}
/**
* {@inheritDoc}
*/
@Override
public Pair<Trunk, String> parseDialTarget(final Integer tenantId, final String calledNumber) {
final Sequence trunkSeq = sequenceMapper.find(
new Search(Sequence.TENANT_ID, tenantId)
.eq(Sequence.NAME, seqName)
.eq(Sequence.ACTIVE, true));
final int prefixLength = trunkSeq.getLength();
if (calledNumber.length() > prefixLength) {
final String prefix = calledNumber.substring(0, prefixLength);
final Trunk trunk = mapper.find(
new Search(Trunk.TENANT_ID, tenantId)
.eq(Trunk.PREFIX, prefix)
.eq(Trunk.ACTIVE, true));
if (trunk != null) {
return Pair.of(trunk, calledNumber.substring(prefixLength));
}
}
return null;
}
private String addPrefix(final Trunk trunk, String number) {
final String gatewayPrefix = trunk.getGatewayPrefix();
return StringUtils.isNotBlank(gatewayPrefix) ? gatewayPrefix + number : number;

View File

@ -12,20 +12,23 @@ import org.freeswitch.esl.client.transport.event.Event;
* @author Donghuang
* @date Jul 08, 2020 11:42:21
*/
public class ChannelNameUtils {
public class CallStrUtils {
public static final Pattern PATTERN_USER_AND_DOMAIN =
Pattern.compile("^sofia/internal/([^@]+)@(.+)$");
public static final Pattern PATTERN_EXTERNAL_NUMBER =
Pattern.compile("^sofia/external/(\\w+)$");
public static final Pattern PATTERN_DIAL_TARGET =
Pattern.compile("^(\\d+)\\.(A|Q|(?:AG))$");
/**
* get channel info
*
* @param event event
* @return channel info
*/
public static ChannelInfo get(final Event event) {
return parse(event.getChannelName());
public static ChannelInfo getChannelInfo(final Event event) {
return parseChannelName(event.getChannelName());
}
/**
@ -34,7 +37,7 @@ public class ChannelNameUtils {
* @param channelName channel name
* @return channel info
*/
public static ChannelInfo parse(final String channelName) {
public static ChannelInfo parseChannelName(final String channelName) {
Matcher m = PATTERN_USER_AND_DOMAIN.matcher(channelName);
if (m.find()) {
return new ChannelInfo(m.group(1), m.group(2), true);
@ -49,6 +52,18 @@ public class ChannelNameUtils {
"Invalid channel name: " + channelName);
}
public static DialTarget parseDialTarget(final String target) {
Matcher m = PATTERN_DIAL_TARGET.matcher(target);
if (m.find()) {
final String type = m.group(2);
return new DialTarget(Integer.parseInt(m.group(1)),
"A".equals(type) ? DialTarget.Type.AGENT :
"AG".equals(type) ? DialTarget.Type.AGENT_GROUP :
DialTarget.Type.QUEUE);
}
return null;
}
@Getter
@Setter
@ToString
@ -58,4 +73,19 @@ public class ChannelNameUtils {
private final String domain;
private final boolean agent;
}
@Getter
@Setter
@ToString
@RequiredArgsConstructor
public static class DialTarget {
private final Integer id;
private final Type type;
public enum Type {
AGENT,
AGENT_GROUP,
QUEUE
}
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml">
<section name="dialplan" description="Dialplan Trunk Outbound">
<context name="${tenant.realm}">
<extension name="TrunkOutbound">
<condition>
<!-- <#noparse> -->
<action application="set" data="odbc-cdr-ignore-leg=true" />
<action application="set" data="call_timeout=30" />
<action application="set" data="hangup_after_bridge=true" />
<action application="set" data="continue_on_fail=true" />
<action application="set" data="media_bug_answer_req=true" />
<action application="set" data="RECORD_STEREO=true" />
<!-- </#noparse> -->
<action application="set" data="x_role=CALLER" />
<action application="export" data="x_conn_id=${connId}" />
<action application="export" data="x_tenant_id=${tenant.id}" />
<action application="export" data="x_tenant_code=${tenant.code}" />
<action application="export" data="x_realm=${tenant.realm}" />
<action application="export" data="x_dial_type=MANUAL" />
<action application="export" data="x_call_type=OUTBOUND" />
<action application="set" data="x_agent_type=${callerAgent.type}" />
<!-- Recording -->
<action application="set" data="record_post_process_exec_api=curl:${postRecUrl} post tenantId=${tenant.id}&loc=${recordingLoc}" />
<action application="record_session" data="${recordingLoc}" />
<!-- /Recording -->
<action application="bridge" data="[origination_uuid=${connId},sip_invite_call_id=${connId},odbc-cdr-ignore-leg=false,sip_contact_user=${r'${caller_id_number}'},x_role=CALLED,x_agent_type=${calledAgent.type},effective_caller_id_number=${trunk.cpn}]sofia/gateway/${gatewayName}/${targetNumber}" />
</condition>
</extension>
</context>
</section>
</document>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml">
<section name="result">
<section name="${section}" description="No valid content found">
<result status="not found" />
</section>
</document>

View File

@ -2,7 +2,7 @@ package com.pudonghot.yo.fsagent;
import org.junit.Test;
import lombok.extern.slf4j.Slf4j;
import com.pudonghot.yo.fsagent.util.ChannelNameUtils;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
/**
* @author Donghuang
@ -13,7 +13,7 @@ public class ChannelNameUtilsTest {
@Test
public void testParse() {
log.info("User and domain [{}]", ChannelNameUtils.parse("sofia/internal/700001@d1.wacai.info"));
log.info("External number [{}]", ChannelNameUtils.parse("sofia/external/013764268709"));
log.info("User and domain [{}]", CallStrUtils.parseChannelName("sofia/internal/700001@d1.wacai.info"));
log.info("External number [{}]", CallStrUtils.parseChannelName("sofia/external/013764268709"));
}
}

View File

@ -22,6 +22,8 @@ public class Sequence extends BaseDomain {
@NotUpdate
private String name;
@NotUpdate
private int length;
@NotUpdate
private Long initVal;
private Long step;
private Long currentVal;

View File

@ -28,8 +28,7 @@ public class AgentStatusController implements SessionAbility {
@Autowired
private AgentEventService agentEventService;
@RequestMapping({"/", "/forcelogin"
})
@RequestMapping({"/", "/forcelogin"})
public RespAgentReady signIn(
@PathVariable("account")
final String account) {

View File

@ -60,6 +60,12 @@ export default (function() {
route: 'agent.list',
icon: 'fa-user',
text: '坐席管理'
},
{
perm: 'PERM_VIEW_AGENT_STATUS_LIST',
route: 'agent-status.list',
icon: 'fa-user',
text: '坐席状态管理'
}
]
}, {
@ -70,13 +76,7 @@ export default (function() {
route: 'campaign.list',
icon: 'fa-fax',
text: '外呼活动管理'
},
{
perm: 'PERM_VIEW_CAMPAIGN_LIST',
route: 'calling-list.list',
icon: 'fa fa-list-alt',
text: '拨打列表'
},
}
]
}, {
text: '公共',

View File

@ -97,25 +97,19 @@ Router.map(function() {
this.route('edit', {path: '/:id/edit'});
});
this.route('campaign-stat', function() {
this.route('list');
});
this.route('trunk-attr', function() {
this.route('list');
this.route('create');
this.route('edit', {path: '/:id/edit'});
});
this.route('calling-list', function() {
this.route('list');
this.route('create');
this.route('edit', {path: '/:id/edit'});
});
this.route('call-detail-record', function() {
this.route('list');
});
this.route('agent-status', function() {
this.route('list');
});
});
export default Router;

View File

@ -0,0 +1,6 @@
import BaseListRoute from './../base-list';
export default BaseListRoute.extend({
perm: 'PERM_VIEW_AGENT_STATUS_LIST',
breadcrumbs: [{text: '坐席状态列表'}],
});

View File

@ -1,34 +0,0 @@
import BaseRoute from '../base';
import RSVP from 'rsvp';
export default BaseRoute.extend({
perm: 'PERM_VIEW_CAMPAIGN_CREATE',
breadcrumbs: [{route: 'calling-list.list', text: '拨打列表'}, {text: '创建拨打名单'}],
model() {
const store = this.get('store');
return RSVP.hash({
active: true,
privacy: true,
campaignList: store.ajaxGet('campaign/list'),
callingListStatus: store.ajaxGet('calling-list/status'),
timeList:[{value:'25200', text:'7:00'},
{value:'28800', text:'8:00'},
{value:'32400', text:'9:00'},
{value:'36000', text:'10:00'},
{value:'39600', text:'11:00'},
{value:'43200', text:'12:00'},
{value:'46800', text:'13:00'},
{value:'50400', text:'14:00'},
{value:'54000', text:'15:00'},
{value:'57600', text:'16:00'},
{value:'61200', text:'17:00'},
{value:'64800', text:'18:00'},
{value:'68400', text:'19:00'},
{value:'72000', text:'20:00'},
{value:'75600', text:'21:00'},
{value:'79200', text:'22:00'},
{value:'82800', text:'23:00'}
]
});
}
});

View File

@ -1,28 +0,0 @@
import BaseEditRoute from '../base-edit';
export default BaseEditRoute.extend({
perm: 'PERM_VIEW_CAMPAIGN_EDIT',
afterModel(model) {
this.set('breadcrumbs',
[{route: 'calling-list.list', text: '拨打列表'},
{text: '编辑拨打名单'}]);
model.timeList=[{value:'25200', text:'7:00'},
{value:'28800', text:'8:00'},
{value:'32400', text:'9:00'},
{value:'36000', text:'10:00'},
{value:'39600', text:'11:00'},
{value:'43200', text:'12:00'},
{value:'46800', text:'13:00'},
{value:'50400', text:'14:00'},
{value:'54000', text:'15:00'},
{value:'57600', text:'16:00'},
{value:'61200', text:'17:00'},
{value:'64800', text:'18:00'},
{value:'68400', text:'19:00'},
{value:'72000', text:'20:00'},
{value:'75600', text:'21:00'},
{value:'79200', text:'22:00'},
{value:'82800', text:'23:00'}
]
}
});

View File

@ -1,6 +0,0 @@
import BaseListRoute from './../base-list';
export default BaseListRoute.extend({
perm: 'PERM_VIEW_CAMPAIGN_LIST',
breadcrumbs: [{text: '拨打列表'}]
});

View File

@ -7,6 +7,7 @@ export default BaseRoute.extend({
return {
active: true,
initVal: 1,
length: 4,
step: 1
};
}

View File

@ -1,14 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'NlpComponentGroup',
constraints: {
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
}
}
});

View File

@ -1,32 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'NlpComponent',
createConstraints: {
groupId: {
presence: true
},
type: {
presence: true
},
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
}
},
constraints: {
groupId: {
presence: true
},
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
}
}
});

View File

@ -1,15 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'NlpNodeSample',
constraints: {
sampleText: {
presence: true,
}
},
batchCreateConstraints: {
sampleTexts: {
presence: true
}
},
});

View File

@ -1,25 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'NlpNode',
constraints: {
componentId: {
presence: true
},
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
},
nodeIndex: {
presence: true,
numericality: {
onlyInteger: true,
greaterThanOrEqualTo: 0,
lessThanOrEqualTo: 128
}
}
}
});

View File

@ -1,14 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'NlpTermGroup',
constraints: {
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
}
}
});

View File

@ -1,24 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'NlpTerm',
constraints: {
groupId: {
presence: true
},
term: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
},
pinyin: {
presence: true,
length: {
minimum: 1,
maximum: 128
}
}
}
});

View File

@ -10,6 +10,14 @@ export default Service.extend({
maximum: 36
}
},
length: {
presence: true,
numericality: {
onlyInteger: true,
greaterThan: 1,
lessThanOrEqualTo: 16
}
},
initVal: {
presence: true,
numericality: {
@ -26,14 +34,7 @@ export default Service.extend({
}
}
},
createConstraints: {
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
},
updateConstraints: {
step: {
presence: true,
numericality: {

View File

@ -1,53 +0,0 @@
import BaseService from '../service';
export default BaseService.extend({
modelName: 'TalkComponent',
constraints: {
note: {
presence: true
},
content: {
presence: true
},
},
voiceConstraints: {
note: {
presence: true
},
voice: {
presence: true
}
},
ttsConstraints: {
text: {
presence: true
},
ttsVoiceActor: {
presence: true
},
note: {
presence: true
},
volume: {
presence: true,
numericality: {
notLessThan: -500,
notGreaterThan: 500
}
},
speechRate: {
presence: true,
numericality: {
notLessThan: -500,
notGreaterThan: 500
}
},
pitchRate: {
presence: true,
numericality: {
notLessThan: -500,
notGreaterThan: 500
}
}
}
});

View File

@ -1,14 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'TalkGroup',
constraints: {
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
}
}
});

View File

@ -1,57 +0,0 @@
import BaseService from '../service';
export default BaseService.extend({
modelName: 'Talk',
normalConstraints: {
talk: {
presence: true
}
},
audioConstraints: {
overwriteStrategy: {
presence: true
},
files: {
presence: true
}
},
ttsConstraints: {
talk: {
presence: true
},
ttsVoiceActor: {
presence: true
},
volume: {
presence: true,
numericality: {
notLessThan: -500,
notGreaterThan: 500
}
},
speechRate: {
presence: true,
numericality: {
notLessThan: -500,
notGreaterThan: 500
}
},
pitchRate: {
presence: true,
numericality: {
notLessThan: -500,
notGreaterThan: 500
}
}
},
updateConstraints: {
talk: {
presence: true
}
},
importConstraints: {
archive: {
presence: true
}
}
});

View File

@ -1,21 +0,0 @@
import Service from '../service';
export default Service.extend({
modelName: 'VoiceActor',
constraints: {
name: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
},
code: {
presence: true,
length: {
minimum: 1,
maximum: 36
}
}
}
});

View File

@ -0,0 +1,88 @@
<div class="widget-box transparent" style="padding-top: 2px; border: 1px solid #ddd;">
{{grid-header search-box=false}}
<div class="widget-body">
<!-- #section:custom/scrollbar -->
<div class="widget-main no-padding">
<table class="table table-striped table-bordered table-hover dataTable">
<thead class="thin-border-bottom">
<tr>
<th>
账户
</th>
<th>
分机
</th>
<th>
{{th-filter name='registerd'
text='注册状态'
options=(array
(hash value=true text='在线')
(hash value=false text='离线')
)
}}
</th>
<th>
{{th-filter name='status'
text='坐席状态'
options=(array
(hash value='READY' text='就绪')
(hash value='NOT_READY' text='未就绪')
(hash value='OFFLINE' text='离线')
)
}}
</th>
<th>
{{th-filter name='state'
text='话机状态'
options=(array
(hash value='IDLE' text='空闲')
(hash value='IN_A_CALL' text='通话中')
(hash value='ACW' text='话后事务')
)
}}
</th>
<th>
通话ID
</th>
<th>
事件Key
</th>
</tr>
</thead>
<tbody>
{{#each model.data as |it|}}
<tr>
<td>
{{it.account}}
</td>
<td>
{{it.agent}}
</td>
<td>
{{status-cell model=it
field='registered'
enabled-text='在线'
disabled-text='离线'}}
</td>
<td>
{{it.status}}
</td>
<td>
{{it.state}}
</td>
<td>
{{it.callUuid}}
</td>
<td>
{{it.eventKey}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{pagination-bar}}
</div>
</div>
{{outlet}}

View File

@ -1,41 +0,0 @@
{{#form-content}}
<hr />
{{form-input-select
name='campaignId'
label='外呼活动'
options=model.campaignList.data
value-field='id'
text-field='name'
enabled-field='active'
}}
{{form-input name='phone' label='号码'}}
{{form-input-select
name='status'
label='状态'
options=model.callingListStatus
value-field='value'
text-field='label'
}}
{{form-input-select
name='dailyFrom'
label='开始时间'
options=model.timeList
value-field='value'
text-field='text'
}}
{{form-input-select
name='dailyTo'
label='结束时间'
options=model.timeList
value-field='value'
text-field='text'
}}
{{form-input name='taskKey' label='任务KEY'}}
{{form-input name='recordKey' label='记录KEY'}}
{{form-input type='textarea' name='recordData' label='记录数据' placeholder='请在此输入业务数据,例如:{"username": "东皇", "gender": "MALE"}'}}
{{form-input name='attachedData' label='随路数据'}}
{{form-input name='note' label='备注'}}
<hr />
{{form-footer-buttons type='create'}}
{{/form-content}}
{{outlet}}

View File

@ -1,60 +0,0 @@
{{#form-content}}
<hr />
{{form-input type='hidden' name='id'}}
{{form-input-select
name='campaignId'
label='外呼活动'
options=model.campaignList
value-field='id'
text-field='name'
enabled-field='active'
}}
{{form-input name='phone' label='号码'}}
{{form-input-select
name='status'
label='状态'
options=model.callingListStatus
value-field='value'
text-field='label'
}}
{{form-input-select
name='dailyFrom'
label='开始时间'
options=model.timeList
value-field='value'
text-field='text'
}}
{{form-input-select
name='dailyTo'
label='结束时间'
options=model.timeList
value-field='value'
text-field='text'
}}
{{form-input name='callUuid' label='通话ID'}}
{{form-input-datetimepicker
labelClass='col-sm-3 col-md-3'
inputClass='col-sm-5 col-md-5'
name='callStartTime'
label='通话开始时间'}}
{{form-input-datetimepicker
labelClass='col-sm-3 col-md-3'
inputClass='col-sm-5 col-md-5'
name='callEstablishedTime'
label='通话接通时间'}}
{{form-input-datetimepicker
labelClass='col-sm-3 col-md-3'
inputClass='col-sm-5 col-md-5'
name='callEndTime'
label='通话结束时间'}}
{{form-input name='callResult' label='通话结果'}}
{{form-input name='taskKey' label='任务KEY'}}
{{form-input name='recordKey' label='记录KEY'}}
{{form-input type='textarea' name='recordData' label='记录数据' placeholder='请在此输入业务数据,例如:{"username": "东皇", "gender": "MALE"}'}}
{{form-input name='attachedData' label='随路数据'}}
{{form-input-enabled}}
{{form-input name='note' label='备注'}}
<hr />
{{form-footer-buttons type='update'}}
{{/form-content}}
{{outlet}}

View File

@ -1,137 +0,0 @@
<div class="widget-box transparent" style="padding-top: 2px; border: 1px solid #ddd;">
{{#grid-header}}
{{#has-perm 'PERM_VIEW_CAMPAIGN_CREATE'}}
<li>
{{#link-to 'calling-list.create'}}
<i class="ace-icon fa fa-plus-circle bigger-110 green"></i>
新建名单
{{/link-to}}
</li>
{{/has-perm}}
{{/grid-header}}
<div class="widget-body">
<!-- #section:custom/scrollbar -->
<div class="widget-main no-padding">
<table class="table table-striped table-bordered table-hover dataTable">
<thead class="thin-border-bottom">
<tr>
<th>
{{th-filter name='campaignId'
text='外呼活动'
options=model.campaignList
value-field='id'
text-field='name'
}}
</th>
<th>
号码
</th>
<th>
{{th-filter name='status'
text='状态'
options=model.callingListStatus
value-field='value'
text-field='label'
}}
</th>
<th>
开始时间
</th>
<th>
结束时间
</th>
<th>
通话ID
</th>
<th>
拨打开始时间
</th>
<th>
拨打接通时间
</th>
<th>
拨打结束时间
</th>
<th>
拨打结果
</th>
<th>
任务KEY
</th>
<th>
记录KEY
</th>
<th>
创建时间
</th>
<th>
<i class="ace-icon fa fa-cogs bigger-110 hidden-480"></i>
管理
</th>
</tr>
</thead>
<tbody>
{{#each model.data as |it|}}
<tr>
<td>
{{it.campaign.name}}
</td>
<td>
{{it.phone}}
</td>
<td>
{{it.status}}
</td>
<td>
{{second-time timeInSecond=it.dailyFrom}}
</td>
<td>
{{second-time timeInSecond=it.dailyTo}}
</td>
<td>
{{it.callUuid}}
</td>
<td>
{{date-cell value=it.callStartTime format='YYYY-MM-DD H:mm:ss'}}
</td>
<td>
{{date-cell value=it.callEstablishedTime format='YYYY-MM-DD H:mm:ss'}}
</td>
<td>
{{date-cell value=it.callEndTime format='YYYY-MM-DD H:mm:ss'}}
</td>
<td>
{{it.callResult}}
</td>
<td>
{{it.taskKey}}
</td>
<td>
{{it.recordKey}}
</td>
<td>
{{date-cell value=it.createdTime format='YYYY-MM-DD H:mm:ss'}}
</td>
<td>
<div class="btn-group">
{{#has-perm 'PERM_VIEW_CAMPAIGN_EDIT'}}
{{status-toggle-button model=it}}
{{edit-btn route-name='calling-list.edit' model-id=it.id perm='PERM_VIEW_CAMPAIGN_EDIT'}}
{{/has-perm}}
{{#has-perm 'PERM_VIEW_CAMPAIGN_DELETE'}}
{{#unless it.active}}
{{delete-button model=it}}
{{/unless}}
{{/has-perm}}
</div>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{pagination-bar}}
</div>
</div>
{{outlet}}

View File

@ -140,13 +140,6 @@
{{delete-button model=it}}
{{/unless}}
{{/has-perm}}
{{#link-to 'calling-list.list'
(query-params filters=(concat '{"campaignId":[' it.id ']}'))
class='btn btn-xs btn-info'
data-rel='tooltip'
title='拨打列表'}}
<i class="ace-icon fa fa-sliders bigger-120"></i>
{{/link-to}}
</div>
</td>
</tr>

View File

@ -1,6 +1,7 @@
{{#form-content}}
<hr />
{{form-input name='name' label='名称'}}
{{form-input name='length' label='长度'}}
{{form-input name='initVal' label='初始值'}}
{{form-input name='step' label='递增值'}}

View File

@ -1,6 +1,7 @@
{{#form-content}}
{{form-input type='hidden' name='id'}}
{{form-input name='name' label='名称'}}
{{form-input name='name' label='名称' readonly=true}}
{{form-input name='length' label='长度' readonly=true}}
{{form-input name='initVal' label='初始值' readonly=true}}
{{form-input name='step' label='递增值'}}

View File

@ -19,6 +19,9 @@
<th>
名称
</th>
<th>
长度
</th>
<th>
当前值
</th>
@ -53,6 +56,9 @@
<td>
{{it.name}}
</td>
<td>
{{it.length}}
</td>
<td>
{{it.currentVal}}
</td>

View File

@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | agent-status/list', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:agent-status/list');
assert.ok(route);
});
});