增加重呼

This commit is contained in:
Shaun Chyxion 2020-09-21 00:21:04 +08:00
parent 97fcf9392a
commit bc57ca2d3d
21 changed files with 506 additions and 50 deletions

View File

@ -8,7 +8,6 @@ import java.util.ArrayList;
import com.wacai.tigon.form.FormList;
import com.wacai.tigon.mybatis.Search;
import com.wacai.tigon.model.ViewModel;
import com.pudonghot.yo.mapper.CampaignMapper;
import com.pudonghot.yo.model.domain.Campaign;
import com.wacai.tigon.web.annotation.ListApi;
import com.wacai.tigon.web.controller.ArgQuery;
@ -16,12 +15,18 @@ import com.pudonghot.yo.cms.auth.SessionAbility;
import com.wacai.tigon.web.annotation.FilterCol;
import com.pudonghot.yo.model.domain.CallingList;
import org.springframework.stereotype.Controller;
import com.pudonghot.yo.cms.service.CampaignService;
import com.pudonghot.yo.cms.annotation.TenantResource;
import com.pudonghot.yo.cms.service.CallingListService;
import com.pudonghot.yo.model.domain.CallingListPrepared;
import com.pudonghot.yo.model.dbobject.CallingListSummary;
import com.wacai.tigon.web.controller.BaseCrudController;
import com.pudonghot.yo.cms.response.RespCallingListSummary;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.pudonghot.yo.cms.form.update.UpdateFormCallingList;
import com.pudonghot.yo.cms.form.create.CreateFormCallingList;
import org.springframework.web.bind.annotation.RequestMapping;
import com.pudonghot.yo.cms.service.CallingListPreparedService;
import org.springframework.beans.factory.annotation.Autowired;
/**
@ -47,7 +52,9 @@ public class CallingListController
CreateFormCallingList,
UpdateFormCallingList> implements SessionAbility {
@Autowired
private CampaignMapper campaignMapper;
private CampaignService campaignService;
@Autowired
private CallingListPreparedService callingListPreparedService;
/**
* {@inheritDoc}
@ -56,21 +63,20 @@ public class CallingListController
protected void after(final ArgQuery<?> arg) {
super.after(arg);
if (arg.getType() == ArgQuery.Type.LIST) {
final ViewModel<List<ViewModel<CallingList>>> vmList =
(ViewModel<List<ViewModel<CallingList>>>) arg.getResult();
val vmList = (ViewModel<List<ViewModel<CallingList>>>) arg.getResult();
vmList.getData().forEach(vm ->
vm.setAttr("campaign", campaignMapper.find(new Search(vm.getData().getCampaignId())))
vm.setAttr("campaign", campaignService.find(new Search(vm.getData().getCampaignId())))
);
}
final ViewModel<?> vm = arg.getResult();
vm.setAttr("campaignList", campaignMapper.list(new Search(Campaign.ACTIVE, true)));
val vm = arg.getResult();
vm.setAttr("campaignList", campaignService.list(new Search(Campaign.ACTIVE, true)));
vm.setAttr("callingListStatus", status());
}
@RequestMapping("/status")
public List<Map<String, Object>> status() {
CallingList.Status[] statusArr = CallingList.Status.values();
List<Map<String, Object>> statusList = new ArrayList<>(statusArr.length);
val statusArr = CallingList.Status.values();
val statusList = new ArrayList<Map<String, Object>>(statusArr.length);
for (CallingList.Status status : CallingList.Status.values()) {
statusList.add(
new HashMap<String, Object>(4) {{
@ -82,14 +88,25 @@ public class CallingListController
}
@RequestMapping("/summary")
public RespCallingListSummary summary() {
// TODO summary
val summary = new RespCallingListSummary();
summary.setTotal(queryService.count(null));
summary.setCalled(queryService.count(
new Search(CallingList.STATUS,
CallingList.Status.CALLED)));
public ViewModel<CallingListSummary> summary(@RequestParam("campaignId") Integer campaignId) {
val vm = new ViewModel<>(((CallingListService) queryService).summary(campaignId));
vm.setAttr("prepared", callingListPreparedService.count(
new Search(CallingListPrepared.CAMPAIGN_ID, campaignId)
.eq(CallingListPrepared.ACTIVE, true)));
return vm;
}
return summary;
@PostMapping("/redial")
public int redial(@RequestParam("campaignId") Integer campaignId) {
return ((CallingListService) queryService).redial(campaignId);
}
@PostMapping("/import-prepared")
public int importPrepared(
@RequestParam("campaignId")
Integer campaignId,
@RequestParam("limit")
Integer limit) {
return ((CallingListService) queryService).importPrepared(campaignId, limit);
}
}

View File

@ -0,0 +1,12 @@
package com.pudonghot.yo.cms.service;
import com.wacai.tigon.service.BaseCrudService;
import com.pudonghot.yo.model.domain.CallingListPrepared;
/**
* @author Donghuang
* @date Sep 20, 2020 14:42:24
*/
public interface CallingListPreparedService
extends BaseCrudService<Integer, CallingListPrepared> {
}

View File

@ -1,16 +1,41 @@
package com.pudonghot.yo.cms.service;
import com.pudonghot.yo.cms.form.create.CreateFormCallingList;
import com.pudonghot.yo.cms.form.update.UpdateFormCallingList;
import com.pudonghot.yo.model.domain.CallingList;
import com.wacai.tigon.service.BaseCrudByFormService;
import com.pudonghot.yo.model.dbobject.CallingListSummary;
import com.pudonghot.yo.cms.form.create.CreateFormCallingList;
import com.pudonghot.yo.cms.form.update.UpdateFormCallingList;
/**
* @author bingpo
* @date 2020/3/16 下午3:46
* @author Donghuang
* @date Sep 20, 2020 12:12:45
*/
public interface CallingListService
extends BaseCrudByFormService<Integer, CallingList,
CreateFormCallingList,
UpdateFormCallingList> {
/**
* find campaign calling list summary
* @param campaignId
* @return
*/
CallingListSummary summary(Integer campaignId);
/**
* redial calling list
*
* @param campaignId campaign id
* @return redial rows
*/
int redial(Integer campaignId);
/**
* import prepared calling list
*
* @param campaignId campaign id
* @param rows import rows
* @return import rows
*/
int importPrepared(Integer campaignId, Integer rows);
}

View File

@ -0,0 +1,21 @@
package com.pudonghot.yo.cms.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.pudonghot.yo.mapper.CallingListPreparedMapper;
import com.pudonghot.yo.model.domain.CallingListPrepared;
import com.wacai.tigon.service.support.BaseCrudServiceSupport;
import com.pudonghot.yo.cms.service.CallingListPreparedService;
/**
* @author Donghuang
* @date Sep 20, 2020 14:43:56
*/
@Slf4j
@Service
public class CallingListPreparedServiceImpl
extends BaseCrudServiceSupport<Integer,
CallingListPrepared,
CallingListPreparedMapper>
implements CallingListPreparedService {
}

View File

@ -1,21 +1,27 @@
package com.pudonghot.yo.cms.service.impl;
import com.pudonghot.yo.cms.form.create.CreateFormCallingList;
import com.pudonghot.yo.cms.form.update.UpdateFormCallingList;
import com.pudonghot.yo.cms.service.CallingListService;
import com.pudonghot.yo.mapper.CallingListMapper;
import lombok.val;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
import com.wacai.tigon.mybatis.Search;
import org.springframework.util.Assert;
import org.springframework.stereotype.Service;
import com.pudonghot.yo.mapper.CampaignMapper;
import com.pudonghot.yo.model.domain.CallingList;
import com.pudonghot.yo.model.domain.Campaign;
import com.wacai.tigon.service.support.BaseCrudByFormServiceSupport;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.util.Assert;
import com.pudonghot.yo.mapper.CallingListMapper;
import org.apache.commons.lang3.time.DateFormatUtils;
import com.pudonghot.yo.cms.service.CallingListService;
import com.pudonghot.yo.model.domain.CallingListPrepared;
import com.pudonghot.yo.mapper.CallingListPreparedMapper;
import com.pudonghot.yo.model.dbobject.CallingListSummary;
import com.pudonghot.yo.cms.form.create.CreateFormCallingList;
import com.pudonghot.yo.cms.form.update.UpdateFormCallingList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.wacai.tigon.service.support.BaseCrudByFormServiceSupport;
/**
* @author bingpo
* @date 2020/3/16 下午4:00
* @author Donghuang
* @date Sep 20, 2020 14:02:13
*/
@Slf4j
@Service
@ -28,14 +34,54 @@ public class CallingListServiceImpl
implements CallingListService {
@Autowired
private CampaignMapper campaignMapper;
@Autowired
private CallingListPreparedMapper callingListPreparedMapper;
/**
* {@inheritDoc}
*/
@Override
protected void beforeCreate(CreateFormCallingList form, CallingList model) {
protected void beforeCreate(final CreateFormCallingList form, final CallingList model) {
super.beforeCreate(form, model);
final Integer campaignId = form.getCampaignId();
final Campaign campaign = campaignMapper.find(campaignId);
Assert.state(campaign != null, "无效的campaign[ " + campaignId + " ]");
val campaignId = form.getCampaignId();
val campaign = campaignMapper.find(campaignId);
Assert.state(campaign != null,
() -> "无效的campaign[ " + campaignId + " ]");
model.setCampaignKey(campaign.getCampaignKey());
}
/**
* {@inheritDoc}
*/
@Override
public CallingListSummary summary(final Integer campaignId) {
return mapper.summary(campaignId);
}
/**
* {@inheritDoc}
*/
@Override
public int redial(final Integer campaignId) {
return mapper.redial(campaignId);
}
/**
* {@inheritDoc}
*/
@Override
public int importPrepared(final Integer campaignId,
final Integer limit) {
val batchKey = DateFormatUtils.format(
System.currentTimeMillis(),
"yyyyMMddHHmmss") + "_FRESH_DATA";
val update = new HashMap<String, Object>();
update.put(CallingListPrepared.EXPORT_BATCH_KEY, batchKey);
val rows = callingListPreparedMapper.update(update,
new Search(CallingListPrepared.CAMPAIGN_ID, campaignId)
.eq(CallingListPrepared.ACTIVE, true)
.isNull(CallingListPrepared.EXPORT_BATCH_KEY).limit(limit));
mapper.insertPrepared(batchKey);
return rows;
}
}

View File

@ -117,7 +117,7 @@
<!-- ip address to bind to, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
<param name="sip-ip" value="$${local_ip_v4}" />
<param name="hold-music" value="$${hold_music}" />
<param name="apply-nat-acl" value="nat.auto" />
<!--<param name="apply-nat-acl" value="nat.auto" />-->
<!-- (default true) set to false if you do not wish to have called party info in 1XX responses -->
<!-- <param name="cid-in-1xx" value="false" /> -->

View File

@ -28,4 +28,6 @@ if [ -z "$1" ]; then
exit 1
fi
mvn -T 4C clean -pl $1 -am -DskipTests spring-boot:run
mvn -T 4C clean -pl $1 -am -DskipTests \
spring-boot:run

View File

@ -1,11 +1,37 @@
package com.pudonghot.yo.mapper;
import com.pudonghot.yo.model.domain.CallingList;
import com.wacai.tigon.mybatis.BaseMapper;
import org.apache.ibatis.annotations.Param;
import com.pudonghot.yo.model.domain.CallingList;
import com.pudonghot.yo.model.dbobject.CallingListSummary;
/**
* @author Donghuang <br>
* Feb 05, 2020 12:55:43
*/
public interface CallingListMapper extends BaseMapper<Integer, CallingList> {
/**
* query campaign calling list summary
*
* @param campaignId campaign id
* @return campaign calling list summary
*/
CallingListSummary summary(@Param("campaignId") Integer campaignId);
/**
* redial not answered
*
* @param campaignId campaign id
* @return effective rows
*/
int redial(@Param("campaignId") Integer campaignId);
/**
* insert prepared calling list
*
* @param exportBatchKey export batch key
* @return
*/
void insertPrepared(@Param("exportBatchKey") String exportBatchKey);
}

View File

@ -9,17 +9,65 @@
-->
<mapper namespace="com.pudonghot.yo.mapper.CallingListMapper">
<select id="statCampaign" resultType="map">
<select id="summary" resultType="com.pudonghot.yo.model.dbobject.CallingListSummary">
select
count(*) count,
count(case when status = 'READY' then 1 end) count_ready,
count(case when status = 'ACHIEVED' then 1 end) count_achieved,
count(case when status = 'CALLED' then 1 end) count_called,
count(case when status = 'CANCELED' then 1 end) count_canceled,
count(case when status = 'CALLED' and connected = 1 then 1 end) count_connected,
count(case when status = 'CALLED' and connected = 0 then 1 end) count_not_connected
from br_calling_list
count(1) total,
count(if (status = 'READY', 1, null)) ready,
count(if (status = 'ACHIEVED', 1, null)) achieved,
count(if (status = 'DIALING', 1, null)) dialing,
count(if (called_times = 1, 1, null)) first_called,
count(if (called_times = 2, 1, null)) twice_called,
<![CDATA[
count(if (
status = 'CALLED'
and called_times = 1
and last_call_duration < 3
and last_call_start_time between now() - interval 12 hour and now() - interval 240 minute,
1, null)) redial,
]]>
count(if (last_connected, 1, null)) answered
from <include refid="table" />
where campaign_id = #{campaignId}
and active = 1
</select>
<update id="redial">
<![CDATA[
update br_calling_list
set status = 'READY',
daily_from = time_to_sec(now()),
daily_to = time_to_sec(now() + interval 60 minute)
where campaign_id = #{campaignId}
and active = 1
and status = 'CALLED'
and called_times = 1
and last_call_duration < 3
and last_call_start_time between now() - interval 12 hour
and now() - interval 240 minute
]]>
</update>
<insert id="insertPrepared">
insert into br_calling_list(
campaign_id, campaign_key,
added_time, task_key,
case_key, phone, status,
case_data, meta_data,
daily_from, daily_to,
note)
select campaign_id, campaign_key,
now() added_time,
export_batch_key task_key,
case_key, phone, 'READY' status,
case_data, meta_data,
time_to_sec('09:30') daily_from,
time_to_sec('12:00') daily_to,
'NEW_DATA' note
from br_calling_list_prepared
where export_batch_key = #{exportBatchKey}
</insert>
</mapper>

View File

@ -0,0 +1,11 @@
package com.pudonghot.yo.mapper;
import com.wacai.tigon.mybatis.BaseMapper;
import com.pudonghot.yo.model.domain.CallingListPrepared;
/**
* @author Donghuang <br>
* Feb 05, 2020 12:55:43
*/
public interface CallingListPreparedMapper extends BaseMapper<Integer, CallingListPrepared> {
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
/**
* @author Donghuang
* @date Sep 20, 2020 13:21:51
*/
-->
<mapper namespace="com.pudonghot.yo.mapper.CallingListPreparedMapper">
</mapper>

View File

@ -1,11 +1,12 @@
package com.pudonghot.yo.mapper;
import lombok.val;
import org.junit.Test;
import lombok.extern.slf4j.Slf4j;
import org.junit.runner.RunWith;
import com.wacai.tigon.mybatis.Search;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
@ -23,4 +24,16 @@ public class CallingListMapperTest {
public void testList() {
mapper.list(new Search());
}
@Test
public void testSummary() {
val summary = mapper.summary(8);
log.info("Summary: {}", summary);
}
@Test
public void testRedial() {
val result = mapper.redial(8);
log.info("Redial: {}", result);
}
}

View File

@ -0,0 +1,26 @@
package com.pudonghot.yo.mapper;
import org.junit.Test;
import lombok.extern.slf4j.Slf4j;
import org.junit.runner.RunWith;
import com.wacai.tigon.mybatis.Search;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Donghuang <br>
* Feb 05, 2020 12:57:01
*/
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:spring/spring-*.xml")
public class CallingListPreparedMapperTest {
@Autowired
private CallingListPreparedMapper mapper;
@Test
public void testList() {
mapper.list(new Search());
}
}

View File

@ -0,0 +1,57 @@
package com.pudonghot.yo.model.dbobject;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
/**
* @author Donghuang
* @date Sep 15, 2020 23:59:44
*/
@Getter
@Setter
@ToString
public class CallingListSummary implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总量
*/
private Integer total;
/**
* 首次拨打
*/
private Integer firstCalled;
/**
* 二次拨打
*/
private Integer twiceCalled;
/**
* 就绪
*/
private Integer ready;
/**
* 加载中
*/
private Integer achieved;
/**
* 拨打中
*/
private Integer dialing;
/**
* 应答
*/
private Integer answered;
/**
* 重呼
*/
private Integer redial;
}

View File

@ -26,6 +26,8 @@ public class CallingList extends BaseDomain {
@NotUpdate
private String campaignKey;
@NotUpdate
private Type type;
@NotUpdate
private Date addedTime;
private DailyTime dailyFrom;
private DailyTime dailyTo;
@ -52,4 +54,9 @@ public class CallingList extends BaseDomain {
public enum Status {
NOT_READY, READY, ACHIEVED, DIALING, CALLED, CANCELED
}
public enum Type {
FRESH,
OLD
}
}

View File

@ -0,0 +1,43 @@
package com.pudonghot.yo.model.domain;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import com.wacai.tigon.mybatis.Table;
import com.wacai.tigon.mybatis.NotUpdate;
import lombok.experimental.FieldNameConstants;
/**
* @author Donghuang
* @date Sep 20, 2020 13:08:26
*/
@Getter
@Setter
@Table("br_calling_list_prepared")
@FieldNameConstants(prefix = "")
public class CallingListPrepared extends BaseDomain {
@NotUpdate
private Integer tenantId;
@NotUpdate
private String tenantCode;
@NotUpdate
private Integer campaignId;
@NotUpdate
private String campaignKey;
@NotUpdate
private Date addedTime;
@NotUpdate
private String phone;
@NotUpdate
private String insertBatchKey;
/**
* 导出到拨打名单Key
*/
private String exportBatchKey;
@NotUpdate
private String caseKey;
@NotUpdate
private String caseData;
@NotUpdate
private String metaData;
}

View File

@ -129,6 +129,10 @@ Router.map(function() {
this.route('create');
this.route('edit', {path: '/:id/edit'});
});
this.route('calling-list', function() {
this.route('summary');
});
});
export default Router;

View File

@ -0,0 +1,32 @@
import BaseRoute from '../base';
export default BaseRoute.extend({
breadcrumbs: [{route: 'campaign.list', text: '外呼活动列表'}, {text: '呼叫名单管理'}],
queryParams: {
campaignId: {
refreshModel: true
}
},
model(params) {
return this.get('service').ajaxGet('summary', params);
},
actions: {
importPrepared() {
console.log('log import prepared.');
},
redial(campaignId) {
const me = this;
console.log('Redial campaign ', campaignId);
if (!me.get('controller.model.redial')) {
me.get('message').warn('没有可重呼的数据');
return;
}
me.get('dialog').confirm('确认要重呼4小时前未接通拨打名单吗', () => {
me.get('service').ajaxPost('redial', {campaignId}).then(count => {
me.get('message').alert('重呼成功,重呼数据量: ' + count);
me.refresh();
});
});
}
}
});

View File

@ -0,0 +1,35 @@
{{#form-content}}
{{!-- {{#form-input label='未入库新数据'}}
<input value="{{model.prepared}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-6" />
<button {{action (route-action 'importPrepared')}} style="margin-left: 2px;" class="btn btn-sm btn-info">导入</button>
{{/form-input}} --}}
{{#form-input label='名单总量'}}
<input value="{{model.total}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-12" />
{{/form-input}}
{{#form-input label='首次拨打量'}}
<input value="{{model.firstCalled}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-12" />
{{/form-input}}
{{#form-input label='二次拨打量'}}
<input value="{{model.twiceCalled}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-12" />
{{/form-input}}
{{#form-input label='应答量'}}
<input value="{{model.answered}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-12" />
{{/form-input}}
{{#form-input label='未拨打'}}
<input value="{{model.ready}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-12" />
{{/form-input}}
{{#form-input label='加载中'}}
<input value="{{model.achieved}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-12" />
{{/form-input}}
{{#form-input label='呼叫中'}}
<input value="{{model.dialing}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-12" />
{{/form-input}}
{{#form-input label='可重呼量'}}
<input value="{{model.redial}}" style="border: 0; background-color: #fff; color: #000;" readonly type="text" class="col-sm-6" />
<button {{action (route-action 'redial' campaignId)}} style="margin-left: 2px;" class="btn btn-sm btn-info">重呼</button>
{{/form-input}}
<hr />
{{!-- {{form-footer-buttons type='update'}} --}}
{{/form-content}}
{{outlet}}

View File

@ -149,6 +149,15 @@
</td>
<td>
<div class="btn-group">
{{#has-perm 'PERM_VIEW_CALLING_LIST_SUMMARY'}}
{{#link-to 'calling-list.summary'
(query-params 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}}
{{/has-perm}}
{{#has-perm 'PERM_VIEW_CAMPAIGN_EDIT'}}
{{status-toggle-button model=it}}
{{edit-btn route-name='campaign.edit' model-id=it.id perm='PERM_VIEW_CAMPAIGN_EDIT'}}

View File

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