Не знаю, пригодиться ли... Выкладываю как есть.
Код:
package ru.provider.sms.tasks;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Calendar;
import java.util.Map;
import bitel.billing.server.contract.bean.BalanceUtils;
import bitel.billing.server.contract.bean.ContractManager;
import bitel.billing.server.npay.Calculator;
import bitel.billing.server.npay.bean.ServiceObjectManager;
import ru.bitel.bgbilling.server.util.ServerUtils;
/**
* <p>Задача рассылки уведомлений о необходимости пополнить счёт</p>
* <p>Ищет абонентов, у которых на счёте недостаточно средств для оплаты следующего месяца, и отправляет им уведомления</p>
* <p>Для запуска класс необходимо скомпилировать и положить к другим библиотекам на сервере биллинга,
* после чего добавить задачу по аналогии с другими задачами планировщика.</p>
*/
public class PeriodicSmsNotifications extends AbstractSmsTask {
private int npayMid;
private BalanceUtils bu;
private Calendar currDate;
private Calendar nextDate;
private Map<Integer, BigDecimal> planAccountMap;
@Override
public String getDescription() {
return this.defaultDescription + "Рассылка SMS уведомлений о необходимости пополнить счёт.";
}
@Override
public boolean initTask() {
boolean r = super.initTask();
this.npayMid = this.taskSetup.getInt("npay.mid", 0);
if (!r || this.npayMid == 0) {
this.log.error("Bad task config!");
return false;
}
return true;
}
@Override
public void executeTask() {
this.con = this.setup.getDBConnectionFromPool();
this.currDate = Calendar.getInstance();
this.nextDate = Calendar.getInstance();
this.nextDate.set(this.currDate.get(Calendar.YEAR), this.currDate.get(Calendar.MONTH) + 1, 1);
if (this.tariffTotal.isEmpty()){
this.tariffTotal = "-1";
}
String query = "" +
" SELECT" +
" c.id AS contractId," +
" c.title AS contractTitle," +
" c.closesumma AS contractLimit," +
" p1.val AS mobilePhone," +
" p2.val AS emailAddress," +
" t.cid" +
" FROM" +
" contract c" +
" LEFT JOIN contract_parameter_type_1 p1 ON c.id = p1.cid AND p1.pid = " + this.mobilePid +
" LEFT JOIN contract_parameter_type_1 p2 ON c.id = p2.cid AND p2.pid = " + this.emailPid +
" LEFT JOIN contract_tariff t ON c.id = t.cid AND t.tpid IN (" + this.tariffTotal + ") AND ((NOW() BETWEEN t.date1 AND t.date2) OR (t.date1 <= NOW() AND t.date2 IS NULL))" +
" WHERE" +
" c.status IN (0,3,4) AND" +
" c.fc = 0 AND (" +
" (p1.val IS NOT NULL AND p1.val != '') OR" +
" (p2.val IS NOT NULL AND p2.val != '')" +
" ) AND t.cid IS NULL";
if (!this.workGroups.isEmpty()) {
query += " AND (";
for (int gid : this.workGroups) {
query += " (c.gr >> " + gid + ") & 1" + " AND";
}
query += " 1=1)";
}
if (!this.skipGroups.isEmpty()) {
query += " AND !(";
for (int gid : this.skipGroups) {
query += " (c.gr >> " + gid + ") & 1" + " OR";
}
query += " 0=1 )";
}
try {
new ContractManager(con);
this.bu = new BalanceUtils(con);
new ServiceObjectManager(con, this.npayMid);
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(query);
String cids = "";
while (rs.next()) {
cids += rs.getInt("contractId");
cids += ",";
}
if (cids.length() == 0) {
this.log.info("Contracts not found");
return;
}
Calculator calculator = new Calculator();
calculator.setExecutingTime( this.nextDate );
calculator.setPreCalc();
calculator.initTask( setup, 0, "mid=" + this.npayMid );
calculator.setCids( cids.substring(0, cids.length() - 1) );
calculator.startTask();
this.planAccountMap = calculator.getCostCache().getContractAccounts();
rs.beforeFirst();
while (rs.next()) {
int contractId = rs.getInt("contractId");
String contractTitle = rs.getString("contractTitle");
BigDecimal contractLimit = rs.getBigDecimal("contractLimit");
String mobilePhone = rs.getString("mobilePhone");
String emailAddress = rs.getString("emailAddress");
sendNotification(contractId, contractTitle, contractLimit, mobilePhone, emailAddress);
sleep();
}
stmt.close();
} catch (Exception ex) {
this.log.error(ex.getMessage(), ex);
} finally {
ServerUtils.closeConnection(con);
}
}
public void sendNotification(int contractId, String contractTitle, BigDecimal contractLimit, String mobilePhone, String emailAddress) throws UnsupportedEncodingException {
BigDecimal balance = this.bu.getBalance(this.currDate.getTime(), contractId);
BigDecimal totalCost = this.planAccountMap.get( contractId );
if (totalCost == null) {
this.log.info("Договор=" + contractTitle + "; limit=" + contractLimit + "; balance=" + balance + "; totalCost=" + totalCost + "; Result=ERROR!");
return;
}
if (totalCost.compareTo(BigDecimal.ZERO) <= 0 || balance.subtract(totalCost).compareTo(contractLimit) >= 0) {
this.log.info("Договор=" + contractTitle + "; limit=" + contractLimit + "; balance=" + balance + "; totalCost=" + totalCost + "; Result=OK!");
} else {
this.log.info("Договор=" + contractTitle + "; limit=" + contractLimit + "; balance=" + balance + "; totalCost=" + totalCost + "; Result=" + totalCost.subtract(balance));
if (emailAddress != null && !emailAddress.isEmpty()) {
String msg = this.emailMsgTpl;
msg = msg.replaceAll("%title%", contractTitle);
msg = msg.replaceAll("%balance%", balance.toString());
msg = msg.replaceAll("%result%", totalCost.subtract(balance).toString());
sendEmailNotification(emailAddress, "Недостаточный баланс", msg);
}
if (mobilePhone != null && !mobilePhone.isEmpty()) {
String mobilePhoneMod = mobilePhone.replaceAll("\\D+", "");
String msg = this.mobileMsgTpl;
msg = msg.replaceAll("%title%", contractTitle);
msg = msg.replaceAll("%balance%", balance.toString());
msg = msg.replaceAll("%result%", totalCost.subtract(balance).toString());
String url = this.urlTpl;
url = url.replaceAll("%mobile%", mobilePhoneMod);
url = url.replaceAll("%message%", URLEncoder.encode(msg, "UTF-8"));
sendSmsNotification(mobilePhoneMod, url, msg);
}
}
}
}
Код:
package ru.provider.sms.tasks;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import bitel.billing.server.util.MailMsg;
import ru.bitel.bgbilling.kernel.task.server.TaskBase;
/**
* <p>Основа для задач рассылки SMS и E-Mail уведомлений абонентам.</p>
*/
public abstract class AbstractSmsTask extends TaskBase {
/**
* <p>Идентификатор приложения для сбора статистики отправленных SMS</p>
*/
protected String appId;
protected int mode;
protected long sleep;
protected String tariffTotal;
protected List<Integer> workGroups;
protected List<Integer> skipGroups;
protected int mobilePid;
protected int emailPid;
protected String urlTpl;
protected String emailMsgTpl;
protected String mobileMsgTpl;
protected Connection con;
@Override
public String getDescription() {
return this.defaultDescription;
}
@Override
public boolean initTask() {
this.appId = this.taskSetup.get("appId", "");
this.mode = this.taskSetup.getInt("mode", 0);
this.sleep = this.taskSetup.getLong("sleep", 0);
this.tariffTotal = this.taskSetup.get("tariff.total", "");
this.workGroups = this.taskSetup.getIntegerList("group.ids", new LinkedList<Integer>());
this.skipGroups = this.taskSetup.getIntegerList("skip.ids", new LinkedList<Integer>());
this.mobilePid = this.taskSetup.getInt("mobile.pid", 0);
this.emailPid = this.taskSetup.getInt("email.pid", 0);
this.urlTpl = this.taskSetup.get("url.tpl", "");
this.emailMsgTpl = this.taskSetup.get("email.msg.tpl", "");
this.mobileMsgTpl = this.taskSetup.get("mobile.msg.tpl", "");
if (this.appId.isEmpty( )|| this.workGroups.isEmpty() || this.emailMsgTpl.isEmpty() || this.mobileMsgTpl.isEmpty() || this.urlTpl.isEmpty() || this.mobilePid == 0 || this.emailPid == 0) {
return false;
}
return true;
}
@Override
public abstract void executeTask();
/**
* <p>Отправка уведомления на электронную почту</p>
* @param address Адрес электронной почты
* @param title Заголовок сообщения
* @param message Текст уведомления
* @throws UnsupportedEncodingException
*/
public void sendEmailNotification(String address, String title, String message) throws UnsupportedEncodingException {
if (this.mode == 0) {
return;
}
this.log.info(" E-Mail Address: " + address);
this.log.info(" E-Mail Text: " + message);
MailMsg emailMsg = new MailMsg(this.setup);
emailMsg.sendMessage(address, title, message);
}
/**
* <p>Отправка уведомления через SMS</p>
* <p>При успешной отправке сообщения по HTTP обновляет счётчит отправленных сообщений для статистики.</p>
* @param mobile Номер телефона в формате 9XXXXXXXXX
* @param url Адрес сервера для отправки запроса
* @param message Текст уведомления
* @throws UnsupportedEncodingException
*/
public void sendSmsNotification(String mobile, String url, String message) throws UnsupportedEncodingException {
if (this.mode == 0) {
return;
}
this.log.info(" SMS Phone: " + mobile);
this.log.info(" SMS Text: " + message);
this.log.info(" SMS URL: " + url);
URLConnection conn;
try {
conn = new URL(url).openConnection();
} catch (MalformedURLException e) {
this.log.error("Malformed URL found: url=" + url);
this.log.error(e.getMessage());
return;
} catch (IOException e) {
this.log.error("IOException while opening the url connection found:");
this.log.error(e.getMessage());
return;
}
String reply;
try {
reply = readStreamToString(conn.getInputStream(), "UTF-8");
} catch (IOException e) {
this.log.error("IOException while reading reply found:");
this.log.error(e.getMessage());
return;
}
this.log.info(" SMS Result: " + reply);
if (reply.matches("^\\s*OK\\s*$")) {
int messages = 0;
if (message.length() <= 70) {
messages = 1;
} else {
messages = (int)Math.ceil((float)message.length()/67);
}
try {
Calendar today = Calendar.getInstance();
PreparedStatement ps = this.con.prepareStatement("" +
" INSERT INTO" +
" sms_stats" +
" VALUES (" +
" ?," +
" ?," +
" ?," +
" ?" +
" ) ON DUPLICATE KEY UPDATE messagesSend = messagesSend + ?");
ps.setInt(1, today.get(Calendar.MONTH) + 1);
ps.setInt(2, today.get(Calendar.YEAR) - 2000);
ps.setString(3, appId);
ps.setInt(4, messages);
ps.setInt(5, messages);
ps.execute();
ps.close();
} catch (SQLException e) {
this.log.error("Cannot update SMS stats: " + e.getMessage());
}
}
}
/**
* <p>Читает данные из потока и записывает их в выходную строку в соответствии с заданной кодировкой.</p>
* @param in Входной поток
* @param encoding Кодировка
* @return
* @throws IOException
*/
public String readStreamToString(InputStream in, String encoding) throws IOException {
StringBuffer b = new StringBuffer();
InputStreamReader r = new InputStreamReader(in, encoding);
int c;
while ((c = r.read()) != -1) {
b.append((char)c);
}
return b.toString();
}
/**
* <p>Приостанавливает выполнение потока на заданное количество секунд.</p>
* @throws InterruptedException
*/
public void sleep() throws InterruptedException {
if (sleep > 0) {
Thread.sleep(sleep);
}
}
}
Конфиг задачи в биллинге:
Код:
# Режим запуска:
# 0 - сообщения не рассылаются, только запись логов работы
# 1 - сообщения рассылаются
mode=1
# Идентификатор приложения для сбора статистики по отправленным SMS-сообщениям
appId=NOTIFICATION
# Время ожидания в милисекундах после обработки одного абонента. 0 - нет ожидания.
sleep=1200
# Код модуля абонплат
npay.mid=2
# Группы договоров, которым рассылать уведомления. Для отправки уведомления у договора должны быть установлены все перечисленные группы одновременно!
group.ids=1,2
# Группы договоров, которым запрещено рассылать уведомления. Для исключения из рассылки у договора должна быть установлена хотя бы одна из перечисленных групп.
skip.ids=3
# Тарифные планы с подневной абонплатой
tariff.total=1,3,4
# Параметр договора, в котором содержится адрес электронной почты
email.pid=1
# Шаблон для отправки электронной почты
email.msg.tpl=Напоминаем пополнить счет на сумму %result% руб. по договору %title%.
# Параметр договора, в котором содержится номер мобильного телефона
mobile.pid=2
# Шаблон SMS сообщения
mobile.msg.tpl=Пополните счет на сумму %result% руб. по договору %title%.
# Шаблон адреса для отправки SMS
url.tpl=http://www.xxx.ru/send.php?phone=%mobile%&message=%message%
В базе нужно добавить:
Код:
SELECT * FROM bgbilling.scheduled_class;
1 Рассылка SMS уведомлений ru.provider.sms.tasks.PeriodicSmsNotifications
Небольшие пояснения.
Задача проверяет, хватает ли денег на следующий месяц, сравнивая планируемую наработку из Calculator с балансом.
Если не хватает, то оправляется SMS и Email сообщения, если указаны номер телефона и/или адрес почты.
Задержка sleep нужна, чтобы звонки абонентов размазать по времени на весь рабочий день.
Для запуска классы нужно скомпилировать, собрать из них jar, положить рядом с kernel.jar. Перезапустить биллинг и планировщик. Добавить в базу одну строчку с новой задачей. Добавить через стандартный интерфейс новую задачу.
Не забываем обращать внимание на версию биллинга.
Из дополнительного здесь ведётся статистика отправленных сообщения для сверки счетов.