add campaign quota

This commit is contained in:
Donghuang 2021-10-20 00:48:56 +08:00
parent 58b460e122
commit 12d2cf49dc
16 changed files with 264 additions and 42 deletions

View File

@ -120,6 +120,16 @@ public class DataWrapper implements Serializable {
return getVal("variable_x_called_number"); return getVal("variable_x_called_number");
} }
/**
* Convenience method.
* get account
*
* @return account
*/
public String getAccount() {
return getVal("variable_x_account");
}
/** /**
* Convenience method. * Convenience method.
* get dial type * get dial type
@ -137,7 +147,7 @@ public class DataWrapper implements Serializable {
* @return campaign id * @return campaign id
*/ */
public Integer getCampaignId() { public Integer getCampaignId() {
final String strCampaignId = getVal("variable_x_campaign_id"); val strCampaignId = getVal("variable_x_campaign_id");
if (StringUtils.isNotBlank(strCampaignId)) { if (StringUtils.isNotBlank(strCampaignId)) {
return Integer.parseInt(strCampaignId); return Integer.parseInt(strCampaignId);
} }

View File

@ -1,11 +1,24 @@
package com.pudonghot.yo.mapper; package com.pudonghot.yo.mapper;
import me.chyxion.tigon.mybatis.BaseMapper; import me.chyxion.tigon.mybatis.BaseMapper;
import org.apache.ibatis.annotations.Param;
import com.pudonghot.yo.model.domain.CampaignQuota; import com.pudonghot.yo.model.domain.CampaignQuota;
import com.pudonghot.yo.mapper.response.CampaignQuotaFindQuotaMapperResp;
/** /**
* @author Donghuang * @author Donghuang
* @date Oct 14, 2021 14:24:45 * @date Oct 14, 2021 14:24:45
*/ */
public interface CampaignQuotaMapper extends BaseMapper<Integer, CampaignQuota> { public interface CampaignQuotaMapper extends BaseMapper<Integer, CampaignQuota> {
/**
* find quota
* @param campaignId campaign id
* @return quota
*/
CampaignQuotaFindQuotaMapperResp findQuota(
@Param("campaignId")
Integer campaignId,
@Param("account")
String account);
} }

View File

@ -9,4 +9,33 @@
"-//mybatis.org//DTD Mapper 3.0//EN" "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pudonghot.yo.mapper.CampaignQuotaMapper"> <mapper namespace="com.pudonghot.yo.mapper.CampaignQuotaMapper">
<select id="findQuota" resultType="com.pudonghot.yo.mapper.response.CampaignQuotaFindQuotaMapperResp">
select account, cq.quota, count(account) called
from <include refid="table" /> cq
join br_call_detail_record rec
on cq.campaign_id = rec.campaign_id
where
<![CDATA[
rec.start_stamp between
date_add(date(now()), interval cq.daily_from second)
and date_add(date(now()), interval cq.daily_to second)
and rec.account = #{account}
and rec.dial_type = 'CAMPAIGN'
and rec.campaign_id = #{campaignId}
and rec.billsec > 0
and cq.campaign_id = #{campaignId}
and time_to_sec(time(now()))
between cq.daily_from and cq.daily_to
group by account, cq.quota
]]>
</select>
</mapper> </mapper>

View File

@ -2,6 +2,7 @@ package com.pudonghot.yo.mapper;
import com.pudonghot.yo.model.domain.QueueAgent; import com.pudonghot.yo.model.domain.QueueAgent;
import me.chyxion.tigon.mybatis.BaseMapper; import me.chyxion.tigon.mybatis.BaseMapper;
import org.apache.ibatis.annotations.Param;
/** /**
* @author Donghuang <br> * @author Donghuang <br>
@ -9,4 +10,11 @@ import me.chyxion.tigon.mybatis.BaseMapper;
*/ */
public interface QueueAgentMapper extends BaseMapper<Integer, QueueAgent> { public interface QueueAgentMapper extends BaseMapper<Integer, QueueAgent> {
/**
* dequeue by account
*
* @param account account
* @return
*/
int dequeue(@Param("account") String account);
} }

View File

@ -8,4 +8,11 @@
*/ */
--> -->
<mapper namespace="com.pudonghot.yo.mapper.QueueAgentMapper"> <mapper namespace="com.pudonghot.yo.mapper.QueueAgentMapper">
<delete id="dequeue">
delete q from <include refid="table" /> q
join br_agent a
on q.agent_id = a.id
and a.account = #{account}
</delete>
</mapper> </mapper>

View File

@ -0,0 +1,19 @@
package com.pudonghot.yo.mapper.response;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
/**
* @author Donghuang
* @date Oct 19, 2021 11:31:17
*/
@Getter
@Setter
@ToString
public class CampaignQuotaFindQuotaMapperResp implements Serializable {
private String account;
private Integer quota;
private Integer called;
}

View File

@ -1,7 +1,9 @@
package com.pudonghot.yo.mapper; package com.pudonghot.yo.mapper;
import lombok.val;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.mybatis.Search; import me.chyxion.tigon.mybatis.Search;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -12,6 +14,7 @@ import org.springframework.test.context.junit4.AbstractTransactionalJUnit4Spring
* @author Donghuang * @author Donghuang
* @date Oct 14, 2021 14:24:45 * @date Oct 14, 2021 14:24:45
*/ */
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TestDriver.class) @SpringBootTest(classes = TestDriver.class)
public class CampaignQuotaMapperTest extends AbstractTransactionalJUnit4SpringContextTests { public class CampaignQuotaMapperTest extends AbstractTransactionalJUnit4SpringContextTests {
@ -22,4 +25,10 @@ public class CampaignQuotaMapperTest extends AbstractTransactionalJUnit4SpringCo
public void mapperTest() { public void mapperTest() {
mapper.list(new Search().limit(8)); mapper.list(new Search().limit(8));
} }
@Test
public void testFindQuota() {
val quota = mapper.findQuota(4, "donghuang");
log.info("Quota [{}].", quota);
}
} }

View File

@ -1,10 +1,12 @@
package com.pudonghot.yo.mapper; package com.pudonghot.yo.mapper;
import lombok.val;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.mybatis.Search; import me.chyxion.tigon.mybatis.Search;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ -14,13 +16,21 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
*/ */
@Slf4j @Slf4j
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:spring/spring-*.xml") @SpringBootTest(classes = TestDriver.class)
public class QueueMapperTest { public class QueueMapperTest {
@Autowired @Autowired
private QueueMapper mapper; private QueueMapper mapper;
@Autowired
private QueueAgentMapper queueAgentMapper;
@Test @Test
public void testList() { public void testList() {
mapper.list(new Search()); mapper.list(new Search());
} }
@Test
public void testDequeue() {
val rtn = queueAgentMapper.dequeue("donghuang");
log.info("Rtn [{}].", rtn);
}
} }

View File

@ -0,0 +1,20 @@
package com.pudonghot.yo.service;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
/**
* @author Donghuang
* @date Oct 19, 2021 14:41:50
*/
public interface CommonCampaignQuotaService {
/**
* agent enqueue
*
* @param campaignId campaign id
* @param account agent
* @return is limited
*/
boolean isLimited(@NotNull Integer campaignId, @NotBlank String account);
}

View File

@ -1,22 +1,33 @@
package com.pudonghot.yo.service; package com.pudonghot.yo.service;
import com.pudonghot.yo.model.domain.Agent; import com.pudonghot.yo.model.domain.Agent;
import javax.validation.constraints.NotBlank;
import com.pudonghot.yo.model.domain.Campaign; import com.pudonghot.yo.model.domain.Campaign;
import org.springframework.validation.annotation.Validated;
/** /**
* @author Donghuang * @author Donghuang
* @date Jul 24, 2020 16:04:44 * @date Jul 24, 2020 16:04:44
*/ */
@Validated
public interface CommonCampaignService { public interface CommonCampaignService {
/**
* find campaign by key
*
* @param campaignKey campaign key
* @return campaign
*/
Campaign findByKey(@NotBlank String campaignKey);
/** /**
* agent enqueue * agent enqueue
* *
* @param campaignKey campaign key * @param queueId queue id
* @param agent agent * @param agent agent
* @return queue id * @return queue id
*/ */
Integer enqueue(String campaignKey, Agent agent); Integer enqueue(Integer queueId, Agent agent);
/** /**
* agent dequeue * agent dequeue
@ -30,6 +41,13 @@ public interface CommonCampaignService {
*/ */
void dequeue(Integer agentId); void dequeue(Integer agentId);
/**
* agent dequeue
*
* @param account account
*/
void dequeue(String account);
/** /**
* start campaign * start campaign
* *

View File

@ -0,0 +1,41 @@
package com.pudonghot.yo.service.impl;
import lombok.val;
import lombok.extern.slf4j.Slf4j;
import com.pudonghot.yo.mapper.CampaignQuotaMapper;
import com.pudonghot.yo.model.domain.CampaignQuota;
import com.wacai.tigon.service.support.BaseServiceSupport;
import com.pudonghot.yo.service.CommonCampaignQuotaService;
/**
* @author Donghuang
* @date Oct 19, 2021 14:45:45
*/
@Slf4j
public class CommonCampaignQuotaServiceImpl
extends BaseServiceSupport<Integer, CampaignQuota, CampaignQuotaMapper>
implements CommonCampaignQuotaService {
/**
* {@inheritDoc}
*/
@Override
public boolean isLimited(final Integer campaignId, final String account) {
log.info("Check campaign [{}] account [{}] is limited.", campaignId, account);
val quota = mapper.findQuota(campaignId, account);
if (quota == null) {
log.info("No campaign quota found.");
return false;
}
val quotaVal = quota.getQuota();
val called = quota.getCalled();
if (called >= quotaVal) {
log.info("Called [{}] greater than quota [{}], account [{}] limited.", called, quotaVal, account);
return true;
}
return false;
}
}

View File

@ -8,7 +8,6 @@ import me.chyxion.tigon.mybatis.Search;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import com.pudonghot.yo.mapper.QueueMapper; import com.pudonghot.yo.mapper.QueueMapper;
import com.pudonghot.yo.model.domain.Agent; import com.pudonghot.yo.model.domain.Agent;
import com.pudonghot.yo.model.domain.Queue;
import com.pudonghot.yo.mapper.CampaignMapper; import com.pudonghot.yo.mapper.CampaignMapper;
import com.pudonghot.yo.model.domain.Campaign; import com.pudonghot.yo.model.domain.Campaign;
import com.pudonghot.yo.mapper.QueueAgentMapper; import com.pudonghot.yo.mapper.QueueAgentMapper;
@ -37,16 +36,25 @@ public class CommonCampaignServiceImpl
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public Integer enqueue(final String campaignKey, final Agent agent) { public Campaign findByKey(final String campaignKey) {
log.info("Agent [{}] enqueue campaign [{}].", agent, campaignKey); val campaign = mapper.find(new Search(Campaign.CAMPAIGN_KEY, campaignKey));
Assert.state(campaign != null,
() -> "No campaign [" + campaignKey + "] found");
Assert.state(campaign.getActive(),
() -> "Campaign [" + campaignKey + "] is not active");
return campaign;
}
val campaign = findValid(campaignKey); /**
val queueId = campaign.getTargetId(); * {@inheritDoc}
*/
@Override
public Integer enqueue(final Integer queueId, final Agent agent) {
val queue = queueMapper.find(queueId); val queue = queueMapper.find(queueId);
Assert.state(queue != null, Assert.state(queue != null,
() -> "Campaign [" + campaignKey + "] queue not found"); () -> "Queue [" + queueId + "] not found");
Assert.state(queue.getActive(), Assert.state(queue.getActive(),
() -> "Campaign [" + campaignKey + "] queue is not active"); () -> "Queue [" + queueId + "] is not active");
val agentId = agent.getId(); val agentId = agent.getId();
val queueAgentExisted = val queueAgentExisted =
queueAgentMapper.find( queueAgentMapper.find(
@ -88,12 +96,21 @@ public class CommonCampaignServiceImpl
queueAgentMapper.delete(new Search(QueueAgent.AGENT_ID, agentId)); queueAgentMapper.delete(new Search(QueueAgent.AGENT_ID, agentId));
} }
/**
* {@inheritDoc}
*/
@Override
public void dequeue(final String account) {
log.info("Agent [{}] dequeue.", account);
queueAgentMapper.dequeue(account);
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void start(final String campaignKey) { public void start(final String campaignKey) {
updateStatus(findValid(campaignKey), Campaign.Status.RUNNING); updateStatus(findByKey(campaignKey), Campaign.Status.RUNNING);
} }
/** /**
@ -101,7 +118,7 @@ public class CommonCampaignServiceImpl
*/ */
@Override @Override
public void stop(final String campaignKey) { public void stop(final String campaignKey) {
updateStatus(findValid(campaignKey), Campaign.Status.STOPPED); updateStatus(findByKey(campaignKey), Campaign.Status.STOPPED);
} }
/** /**
@ -128,14 +145,4 @@ public class CommonCampaignServiceImpl
mapper.update(campaign); mapper.update(campaign);
commonCallDataService.resetCampaignChannel(campaign.getId()); commonCallDataService.resetCampaignChannel(campaign.getId());
} }
private Campaign findValid(final String campaignKey) {
final Campaign campaign =
mapper.find(new Search(Campaign.CAMPAIGN_KEY, campaignKey));
Assert.state(campaign != null,
() -> "No campaign [" + campaignKey + "] found");
Assert.state(campaign.getActive(),
() -> "Campaign [" + campaignKey + "] is not active");
return campaign;
}
} }

View File

@ -10,5 +10,6 @@
<bean class="com.pudonghot.yo.service.impl.CommonChannelServiceImpl" /> <bean class="com.pudonghot.yo.service.impl.CommonChannelServiceImpl" />
<bean class="com.pudonghot.yo.service.impl.CommonCallDataServiceImpl" /> <bean class="com.pudonghot.yo.service.impl.CommonCallDataServiceImpl" />
<bean class="com.pudonghot.yo.service.impl.CommonCampaignServiceImpl" /> <bean class="com.pudonghot.yo.service.impl.CommonCampaignServiceImpl" />
<bean class="com.pudonghot.yo.service.impl.CommonCampaignQuotaServiceImpl" />
<bean class="com.pudonghot.yo.service.impl.LeaderElectionServiceImpl" /> <bean class="com.pudonghot.yo.service.impl.LeaderElectionServiceImpl" />
</beans> </beans>

View File

@ -1,14 +1,16 @@
package com.pudonghot.yo.fsagent.listener; package com.pudonghot.yo.fsagent.listener;
import lombok.val;
import java.util.Date; import java.util.Date;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import com.pudonghot.yo.service.*;
import com.pudonghot.yo.util.LogMDC; import com.pudonghot.yo.util.LogMDC;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.pudonghot.yo.mapper.CallingListMapper; import com.pudonghot.yo.mapper.CallingListMapper;
import com.pudonghot.yo.model.domain.CallingList; import com.pudonghot.yo.model.domain.CallingList;
import com.pudonghot.yo.fsagent.util.CallStrUtils;
import com.pudonghot.yo.fsagent.util.EslEventUtils; import com.pudonghot.yo.fsagent.util.EslEventUtils;
import com.pudonghot.yo.service.CommonCallDataService;
import org.freeswitch.esl.client.transport.event.Event; import org.freeswitch.esl.client.transport.event.Event;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@ -25,6 +27,10 @@ public class CampaignChannelDestroy {
private CommonCallDataService callDataService; private CommonCallDataService callDataService;
@Autowired @Autowired
private CallingListMapper callingListMapper; private CallingListMapper callingListMapper;
@Autowired
private CommonCampaignQuotaService campaignQuotaService;
@Autowired
private CommonCampaignService campaignService;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -37,33 +43,34 @@ public class CampaignChannelDestroy {
LogMDC.setTraceId(event.getCallId()); LogMDC.setTraceId(event.getCallId());
log.debug("On campaign channel destroy event [{}].", event.getHeaders()); log.debug("On campaign channel destroy event [{}].", event.getHeaders());
if (EslEventUtils.isCalled(event)) { val campaignId = event.getCampaignId();
final Integer campaignId = event.getCampaignId();
if (campaignId == null) { if (campaignId == null) {
log.error("Campaign [{}] channel destroyed, but no campaign id found.", log.error("Campaign [{}] channel destroyed, but no campaign id found.",
event.getChannelName()); event.getChannelName());
return; return;
} }
if (EslEventUtils.isCalled(event)) {
callDataService.decrCampaignChannel(campaignId); callDataService.decrCampaignChannel(campaignId);
final String strRecId = event.getRecId(); val strRecId = event.getRecId();
if (StringUtils.isNotBlank(strRecId)) { if (StringUtils.isNotBlank(strRecId)) {
final Integer recId = Integer.parseInt(strRecId); val recId = Integer.parseInt(strRecId);
final String calledNumber = event.getCalledNumber(); val calledNumber = event.getCalledNumber();
log.info("Channel destroy update case [{}:{}] calling list call result.", log.info("Channel destroy update case [{}:{}] calling list call result.",
recId, calledNumber); recId, calledNumber);
final CallingList rec = callingListMapper.find(recId); val rec = callingListMapper.find(recId);
if (rec == null) { if (rec == null) {
log.warn("No case [{}:{}] calling list found, ignore.", strRecId, calledNumber); log.warn("No case [{}:{}] calling list found, ignore.", strRecId, calledNumber);
return; return;
} }
final String strBillSec = event.getHeader("variable_billsec"); val strBillSec = event.getHeader("variable_billsec");
final Long billSec = StringUtils.isNotBlank(strBillSec) ? val billSec = StringUtils.isNotBlank(strBillSec) ?
Long.parseLong(strBillSec) : 0; Long.parseLong(strBillSec) : 0;
rec.setLastCallDuration(billSec); rec.setLastCallDuration(billSec);
rec.setLastConnected(billSec > 0); rec.setLastConnected(billSec > 0);
rec.setLastHangupCause(event.getHangupCause()); rec.setLastHangupCause(event.getHangupCause());
@ -72,5 +79,17 @@ public class CampaignChannelDestroy {
callingListMapper.update(rec); callingListMapper.update(rec);
} }
} }
else {
val channelInfo = CallStrUtils.getChannelInfo(event);
if (channelInfo.isAgent()) {
val account = event.getAccount();
if (StringUtils.isNotBlank(account)) {
if (campaignQuotaService.isLimited(campaignId, account)) {
log.info("Account [{}] reaches campaign [{}] quota, dequeue.", account, campaignId);
campaignService.dequeue(account);
}
}
}
}
} }
} }

View File

@ -1,7 +1,11 @@
package com.pudonghot.yo.openapi.controller; package com.pudonghot.yo.openapi.controller;
import com.pudonghot.yo.model.domain.Campaign;
import com.pudonghot.yo.service.CommonCampaignQuotaService;
import lombok.val; import lombok.val;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import com.pudonghot.yo.service.AgentService; import com.pudonghot.yo.service.AgentService;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@ -32,6 +36,8 @@ public class CampaignController implements SessionAbility {
private AgentEventService agentEventService; private AgentEventService agentEventService;
@Autowired @Autowired
private CommonCampaignService commonCampaignService; private CommonCampaignService commonCampaignService;
@Autowired
private CommonCampaignQuotaService commonCampaignQuotaService;
@RequestMapping("/resource/task/{account}") @RequestMapping("/resource/task/{account}")
public void addToQueue( public void addToQueue(
@ -43,8 +49,13 @@ public class CampaignController implements SessionAbility {
form.getTenantId(), account); form.getTenantId(), account);
agentStatusService.findRegisteredAgentStatus(agent); agentStatusService.findRegisteredAgentStatus(agent);
commonCampaignService.dequeue(agent); commonCampaignService.dequeue(agent);
val queueId = commonCampaignService.enqueue(form.getCampaignKey(), agent); val campaignKey = form.getCampaignKey();
agent.setQueues(String.valueOf(queueId)); val campaign = commonCampaignService.findByKey(campaignKey);
if (!commonCampaignQuotaService.isLimited(campaign.getId(), account)) {
val queueId = commonCampaignService.enqueue(campaign.getTargetId(), agent);
agent.setQueues(String.valueOf(queueId));
}
// agent ready // agent ready
agentStatusService.ready(agent); agentStatusService.ready(agent);

View File

@ -49,13 +49,13 @@ public class AgentServiceImpl
*/ */
@Override @Override
public Agent findCallerAgent(final Event event) { public Agent findCallerAgent(final Event event) {
final Integer tenantId = event.getTenantId(); val tenantId = event.getTenantId();
if (tenantId == null) { if (tenantId == null) {
log.info("No tenant id found in event, ignore."); log.info("No tenant id found in event, ignore.");
return null; return null;
} }
final String account = event.getHeader("variable_x_account"); val account = event.getAccount();
if (StringUtils.isNotBlank(account)) { if (StringUtils.isNotBlank(account)) {
return mapper.find( return mapper.find(
@ -63,7 +63,7 @@ public class AgentServiceImpl
.eq(Agent.ACCOUNT, account)); .eq(Agent.ACCOUNT, account));
} }
final String otherNumber = event.getOtherNumber(); val otherNumber = event.getOtherNumber();
if (StringUtils.isNotBlank(otherNumber)) { if (StringUtils.isNotBlank(otherNumber)) {
return mapper.find( return mapper.find(
new Search(Agent.TENANT_ID, tenantId) new Search(Agent.TENANT_ID, tenantId)
@ -78,7 +78,7 @@ public class AgentServiceImpl
*/ */
@Override @Override
public Agent findOfCalled(final Integer tenantId, final String calledNumber) { public Agent findOfCalled(final Integer tenantId, final String calledNumber) {
final Agent agent = mapper.find( val agent = mapper.find(
new Search(Agent.TENANT_ID, tenantId) new Search(Agent.TENANT_ID, tenantId)
.eq(Agent.AGENT, calledNumber)); .eq(Agent.AGENT, calledNumber));
return agent != null ? agent : return agent != null ? agent :