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

View File

@ -1,11 +1,24 @@
package com.pudonghot.yo.mapper;
import me.chyxion.tigon.mybatis.BaseMapper;
import org.apache.ibatis.annotations.Param;
import com.pudonghot.yo.model.domain.CampaignQuota;
import com.pudonghot.yo.mapper.response.CampaignQuotaFindQuotaMapperResp;
/**
* @author Donghuang
* @date Oct 14, 2021 14:24:45
*/
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"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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>

View File

@ -2,6 +2,7 @@ package com.pudonghot.yo.mapper;
import com.pudonghot.yo.model.domain.QueueAgent;
import me.chyxion.tigon.mybatis.BaseMapper;
import org.apache.ibatis.annotations.Param;
/**
* @author Donghuang <br>
@ -9,4 +10,11 @@ import me.chyxion.tigon.mybatis.BaseMapper;
*/
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">
<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>

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;
import lombok.val;
import org.junit.Test;
import org.junit.runner.RunWith;
import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.mybatis.Search;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;
@ -12,6 +14,7 @@ import org.springframework.test.context.junit4.AbstractTransactionalJUnit4Spring
* @author Donghuang
* @date Oct 14, 2021 14:24:45
*/
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TestDriver.class)
public class CampaignQuotaMapperTest extends AbstractTransactionalJUnit4SpringContextTests {
@ -22,4 +25,10 @@ public class CampaignQuotaMapperTest extends AbstractTransactionalJUnit4SpringCo
public void mapperTest() {
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;
import lombok.val;
import org.junit.Test;
import org.junit.runner.RunWith;
import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.mybatis.Search;
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.junit4.SpringJUnit4ClassRunner;
@ -14,13 +16,21 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
*/
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:spring/spring-*.xml")
@SpringBootTest(classes = TestDriver.class)
public class QueueMapperTest {
@Autowired
private QueueMapper mapper;
@Autowired
private QueueAgentMapper queueAgentMapper;
@Test
public void testList() {
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;
import com.pudonghot.yo.model.domain.Agent;
import javax.validation.constraints.NotBlank;
import com.pudonghot.yo.model.domain.Campaign;
import org.springframework.validation.annotation.Validated;
/**
* @author Donghuang
* @date Jul 24, 2020 16:04:44
*/
@Validated
public interface CommonCampaignService {
/**
* find campaign by key
*
* @param campaignKey campaign key
* @return campaign
*/
Campaign findByKey(@NotBlank String campaignKey);
/**
* agent enqueue
*
* @param campaignKey campaign key
* @param queueId queue id
* @param agent agent
* @return queue id
*/
Integer enqueue(String campaignKey, Agent agent);
Integer enqueue(Integer queueId, Agent agent);
/**
* agent dequeue
@ -30,6 +41,13 @@ public interface CommonCampaignService {
*/
void dequeue(Integer agentId);
/**
* agent dequeue
*
* @param account account
*/
void dequeue(String account);
/**
* 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 com.pudonghot.yo.mapper.QueueMapper;
import com.pudonghot.yo.model.domain.Agent;
import com.pudonghot.yo.model.domain.Queue;
import com.pudonghot.yo.mapper.CampaignMapper;
import com.pudonghot.yo.model.domain.Campaign;
import com.pudonghot.yo.mapper.QueueAgentMapper;
@ -37,16 +36,25 @@ public class CommonCampaignServiceImpl
* {@inheritDoc}
*/
@Override
public Integer enqueue(final String campaignKey, final Agent agent) {
log.info("Agent [{}] enqueue campaign [{}].", agent, campaignKey);
public Campaign findByKey(final String 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);
Assert.state(queue != null,
() -> "Campaign [" + campaignKey + "] queue not found");
() -> "Queue [" + queueId + "] not found");
Assert.state(queue.getActive(),
() -> "Campaign [" + campaignKey + "] queue is not active");
() -> "Queue [" + queueId + "] is not active");
val agentId = agent.getId();
val queueAgentExisted =
queueAgentMapper.find(
@ -88,12 +96,21 @@ public class CommonCampaignServiceImpl
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}
*/
@Override
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
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);
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.CommonCallDataServiceImpl" />
<bean class="com.pudonghot.yo.service.impl.CommonCampaignServiceImpl" />
<bean class="com.pudonghot.yo.service.impl.CommonCampaignQuotaServiceImpl" />
<bean class="com.pudonghot.yo.service.impl.LeaderElectionServiceImpl" />
</beans>

View File

@ -1,14 +1,16 @@
package com.pudonghot.yo.fsagent.listener;
import lombok.val;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import com.pudonghot.yo.service.*;
import com.pudonghot.yo.util.LogMDC;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.pudonghot.yo.mapper.CallingListMapper;
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.service.CommonCallDataService;
import org.freeswitch.esl.client.transport.event.Event;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
@ -25,6 +27,10 @@ public class CampaignChannelDestroy {
private CommonCallDataService callDataService;
@Autowired
private CallingListMapper callingListMapper;
@Autowired
private CommonCampaignQuotaService campaignQuotaService;
@Autowired
private CommonCampaignService campaignService;
/**
* {@inheritDoc}
@ -37,33 +43,34 @@ public class CampaignChannelDestroy {
LogMDC.setTraceId(event.getCallId());
log.debug("On campaign channel destroy event [{}].", event.getHeaders());
if (EslEventUtils.isCalled(event)) {
final Integer campaignId = event.getCampaignId();
val campaignId = event.getCampaignId();
if (campaignId == null) {
log.error("Campaign [{}] channel destroyed, but no campaign id found.",
if (campaignId == null) {
log.error("Campaign [{}] channel destroyed, but no campaign id found.",
event.getChannelName());
return;
}
return;
}
if (EslEventUtils.isCalled(event)) {
callDataService.decrCampaignChannel(campaignId);
final String strRecId = event.getRecId();
val strRecId = event.getRecId();
if (StringUtils.isNotBlank(strRecId)) {
final Integer recId = Integer.parseInt(strRecId);
final String calledNumber = event.getCalledNumber();
val recId = Integer.parseInt(strRecId);
val calledNumber = event.getCalledNumber();
log.info("Channel destroy update case [{}:{}] calling list call result.",
recId, calledNumber);
final CallingList rec = callingListMapper.find(recId);
val rec = callingListMapper.find(recId);
if (rec == null) {
log.warn("No case [{}:{}] calling list found, ignore.", strRecId, calledNumber);
return;
}
final String strBillSec = event.getHeader("variable_billsec");
final Long billSec = StringUtils.isNotBlank(strBillSec) ?
val strBillSec = event.getHeader("variable_billsec");
val billSec = StringUtils.isNotBlank(strBillSec) ?
Long.parseLong(strBillSec) : 0;
rec.setLastCallDuration(billSec);
rec.setLastConnected(billSec > 0);
rec.setLastHangupCause(event.getHangupCause());
@ -72,5 +79,17 @@ public class CampaignChannelDestroy {
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;
import com.pudonghot.yo.model.domain.Campaign;
import com.pudonghot.yo.service.CommonCampaignQuotaService;
import lombok.val;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j;
import com.pudonghot.yo.service.AgentService;
import org.springframework.stereotype.Controller;
@ -32,6 +36,8 @@ public class CampaignController implements SessionAbility {
private AgentEventService agentEventService;
@Autowired
private CommonCampaignService commonCampaignService;
@Autowired
private CommonCampaignQuotaService commonCampaignQuotaService;
@RequestMapping("/resource/task/{account}")
public void addToQueue(
@ -43,8 +49,13 @@ public class CampaignController implements SessionAbility {
form.getTenantId(), account);
agentStatusService.findRegisteredAgentStatus(agent);
commonCampaignService.dequeue(agent);
val queueId = commonCampaignService.enqueue(form.getCampaignKey(), agent);
agent.setQueues(String.valueOf(queueId));
val campaignKey = form.getCampaignKey();
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
agentStatusService.ready(agent);

View File

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