BiTel WiKi - Вклад участника [ru] http://wiki.bitel.ru/index.php/%D0%A1%D0%BB%D1%83%D0%B6%D0%B5%D0%B1%D0%BD%D0%B0%D1%8F:Contributions/Cromeshnic Материал из BiTel WiKi ru MediaWiki 1.15.1 Thu, 28 Mar 2024 17:32:17 GMT Глобальный скрипт для удаления старых таблиц 2 http://wiki.bitel.ru/index.php/%D0%93%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82_%D0%B4%D0%BB%D1%8F_%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D1%81%D1%82%D0%B0%D1%80%D1%8B%D1%85_%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86_2 <p>Cromeshnic:&#32;</p> <hr /> <div>''' Глобальный скрипт для удаления старых таблиц (drop/truncate/dump)'''<br /> <br /> Скрипт удаляет произвольные месячные или дневные таблицы в соответствии с настройками в глобальном конфиге биллинга:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> #Скрипт удаления старых таблиц DropOldTables<br /> script.droptables.debug=0<br /> #уведомление на email со списком удалённых таблиц<br /> script.droptables.email=admin@provider.ru<br /> #храним бэкапы<br /> script.droptables.dirbackup=/mnt/backup/<br /> script.droptables.dumpstring=mysqldump -uroot -pbilling -P 3306 bgbilling<br /> &lt;/source&gt;<br /> <br /> Удаляем (drop=1) или транкейтим (drop=0[default]) месячную таблицу старше 3 месяцев, при необходимости делаем дамп (backup=1):<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.month.1.prefix=log_server_12<br /> script.droptables.month.1.months=3<br /> script.droptables.month.1.drop=0<br /> script.droptables.month.1.backup=0<br /> &lt;/source&gt;<br /> <br /> Удаляем (drop=1) или транкейтим (drop=0[default]) дневную таблицу старше 3 месяцев, при необходимости делаем дамп (backup=1):<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.day.1.prefix=data_log_18<br /> script.droptables.day.1.months=3<br /> script.droptables.day.1.drop=1<br /> script.droptables.day.1.backup=0<br /> &lt;/source&gt;<br /> <br /> Делаем optimize table за 2 месяца, начиная с предыдущего:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.optimize.month.1.prefix=log_session_18<br /> script.droptables.optimize.month.1.months=2<br /> &lt;/source&gt;<br /> <br /> Удаляем записи из session_detail модуля dialup, но только если по ним нет начислений в session_account (удаляем бесплатные трафики). Затем делаем Optimize table. В примере удаляются данные за 1 месяц старше 3 лет:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.dialup.cleanup.1.mid=21<br /> script.droptables.dialup.cleanup.1.months=37<br /> script.droptables.dialup.cleanup.1.depth=1<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.kernel.scripts.global;<br /> <br /> import bitel.billing.server.util.MailMsg;<br /> import ru.bitel.bgbilling.kernel.script.server.dev.GlobalScriptBase;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> <br /> import java.io.IOException;<br /> import java.sql.*;<br /> import java.util.Calendar;<br /> import java.util.Map;<br /> <br /> import static ru.bitel.common.TimeUtils.*;<br /> /**<br /> * Чистим старые таблицы<br /> */<br /> public class DropOldTables<br /> extends GlobalScriptBase<br /> {<br /> @Override<br /> public void execute( Setup setup, ConnectionSet connectionSet )<br /> throws Exception<br /> {<br /> ParameterMap params = setup.sub(&quot;script.droptables.&quot;);<br /> String prefix;<br /> String name;<br /> int months;<br /> int flagDrop = 0;<br /> int backup = 0;<br /> Calendar cal;<br /> StringBuilder report = new StringBuilder();<br /> boolean debug = params.getBoolean(&quot;debug&quot;, false);<br /> String email = params.get(&quot;email&quot;, null);<br /> String backupdir=params.get(&quot;dirbackup&quot;,null);<br /> String dumpstring=params.get(&quot;dumpstring&quot;,null);<br /> <br /> //daily<br /> //Подневные таблицы вида prefix_yyyymmdd<br /> // например, удаляем cdr-логи телефонии старше 6 месяцев (оставляем текущий + 5 предыдущих):<br /> //script.droptables.day.1.prefix=data_log_18<br /> //script.droptables.day.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;day.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> flagDrop = entry.getValue().getInt(&quot;drop&quot;, 0);<br /> backup = entry.getValue().getInt(&quot;backup&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.day.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Удаляем 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=12;i++) {<br /> for (int dd=1; dd&lt;=cal.getActualMaximum(Calendar.DAY_OF_MONTH); dd++){<br /> cal.set(Calendar.DAY_OF_MONTH, dd);<br /> name = prefix + format(cal, &quot;_yyyyMMdd&quot;);<br /> deleteRecord(connectionSet, name, flagDrop, report, backupdir, dumpstring, backup, debug);<br /> }<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> //monthly<br /> //Помесячные таблицы вида prefix_yyyymm<br /> // например, удаляем radius-логи телефонии/dialup старше 6 месяцев (оставляем текущий + 5 предыдущих):<br /> //script.droptables.month.1.prefix=log_server_12<br /> //script.droptables.month.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;month.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> flagDrop = entry.getValue().getInt(&quot;drop&quot;, 0);<br /> backup = entry.getValue().getInt(&quot;backup&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.month.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Удаляем 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=12;i++) {<br /> name = prefix + format(cal, &quot;_yyyyMM&quot;);<br /> deleteRecord(connectionSet, name,flagDrop,report, backupdir, dumpstring, backup, debug);<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> PreparedStatement ps;<br /> <br /> //monthly - optimize tables<br /> //Помесячные таблицы вида prefix_yyyymm<br /> // например, оптимизируем логи телефонии/dialup за 6 месяцев, начиная с предыдущего:<br /> //script.droptables.optimize.month.1.prefix=log_session_18<br /> //script.droptables.optimize.month.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;optimize.month.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.optimize.month.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-1);<br /> //Optimize 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=months;i++) {<br /> name = prefix + format(cal, &quot;_yyyyMM&quot;);<br /> if(tableExists(connectionSet.getConnection(), name)) {<br /> ps = connectionSet.getConnection().prepareStatement(&quot;optimize table &quot;+name);<br /> ps.executeUpdate();<br /> ps.close();<br /> print(&quot;OPTIMIZE TABLE &quot;+name);<br /> report.append(&quot;OPTIMIZE TABLE &quot;).append(name).append(&quot;\n&quot;);<br /> }<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> //monthly - удаляем бесплатные трафики из session_detail_mid_yyyymm модуля dialup<br /> //например, чистим трафики в таблицах старше 3 лет глубиной в 1 месяц и сразу делаем optimize<br /> //script.droptables.dialup.cleanup.1.mid=21<br /> //script.droptables.dialup.cleanup.1.months=36<br /> //script.droptables.dialup.cleanup.1.depth=1<br /> //script.droptables.dialup.cleanup.1.optimize=1<br /> int mid;<br /> int depth;<br /> int optimize;<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;dialup.cleanup.&quot;).entrySet()){<br /> mid = entry.getValue().getInt(&quot;mid&quot;,0 );<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> depth = entry.getValue().getInt(&quot;depth&quot;, 1);<br /> optimize = entry.getValue().getInt(&quot;optimize&quot;, 1);<br /> if(mid&lt;=0 || months&lt;=0){<br /> error(&quot;'mid' or 'months'&lt;=0 for config params script.droptables.dialup.cleanup.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Optimize depth месяцев начиная с now.mm-months<br /> int max_cid;<br /> int min_cid;<br /> int row_count;<br /> int counter;<br /> ResultSet rs;<br /> for(int i=1;i&lt;=depth;i++) {<br /> name = &quot;session_detail_&quot;+mid + format(cal, &quot;_yyyyMM&quot;);<br /> if(tableExists(connectionSet.getConnection(), name)) {<br /> //Будем делать delete кусочками. limit в mysql нельзя использовать в delete с join.<br /> //Поэтому удаляем с отсечкой по индексному полю cid с градацией в 1000 cid-ов<br /> ps = connectionSet.getConnection().prepareStatement(&quot;select max(cid) from &quot;+name);<br /> rs = ps.executeQuery();<br /> max_cid = 0;<br /> if(rs.next()){<br /> max_cid = rs.getInt(1);<br /> }<br /> rs.close();<br /> ps.close();<br /> <br /> ps = connectionSet.getConnection().prepareStatement(&quot;select min(cid) from &quot;+name);<br /> rs = ps.executeQuery();<br /> min_cid = 0;<br /> if(rs.next()){<br /> min_cid = rs.getInt(1);<br /> }<br /> rs.close();<br /> ps.close();<br /> <br /> row_count=0;<br /> counter=0;<br /> for(int id=max_cid-1000; id&gt;min_cid-1000;id-=1000) {<br /> ps = connectionSet.getConnection().prepareStatement(&quot;delete d from &quot; + name + &quot; d &quot; +<br /> &quot;left join session_account_&quot; + mid + format(cal, &quot;_yyyyMM&quot;) + &quot; a on d.session_id=a.session_id and d.sid=a.sid &quot; +<br /> &quot;where a.session_id is null and d.cid&gt;=&quot;+id);<br /> row_count+=ps.executeUpdate();<br /> counter++;<br /> ps.close();<br /> }<br /> print(&quot;* delete ~~~ from TABLE &quot; + name+&quot; [&quot;+row_count+&quot; rows, &quot;+counter+&quot; iterations]&quot;);<br /> report.append(&quot;* delete ~~~ from TABLE &quot;).append(name).append(&quot;\n&quot;);<br /> <br /> if(optimize==1) {<br /> ps = connectionSet.getConnection().prepareStatement(&quot;optimize table &quot; + name);<br /> ps.executeUpdate();<br /> ps.close();<br /> print(&quot;OPTIMIZE TABLE &quot; + name);<br /> report.append(&quot;OPTIMIZE TABLE &quot;).append(name).append(&quot;\n&quot;);<br /> }<br /> }<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> if(email!=null &amp;&amp; !&quot;&quot;.equals(email)){<br /> MailMsg msg = new MailMsg(setup);<br /> msg.sendMessage(email, &quot;Удаление старых таблиц&quot;, report.toString());<br /> }<br /> }<br /> private void deleteRecord(ConnectionSet connectionSet, String tableName, int flagDrop, StringBuilder report, String backupdir, String dumpstring, int backup, boolean debug){<br /> if(tableExists(connectionSet.getConnection(), tableName)) {<br /> String trorDrop=flagDrop==0?&quot;TRUNCATE&quot;:&quot;DROP TABLE&quot;;<br /> String filedm=backupdir+&quot;/&quot;+tableName;<br /> <br /> <br /> print(trorDrop+&quot; &quot; + tableName);<br /> if (!debug){<br /> int exitValue=1;<br /> if(backup==1){<br /> Runtime rt = Runtime.getRuntime();<br /> String commandDump = dumpstring+&quot; &quot;+tableName+&quot; -r &quot;+ filedm+&quot;.sql&quot;;<br /> String commandZip = &quot; zip &quot;+filedm+&quot;.zip &quot;+filedm+&quot;.sql&quot;;<br /> String commandRm = &quot;rm &quot;+filedm+&quot;.sql&quot;;<br /> try {<br /> Process proc=rt.exec(commandDump);<br /> proc.waitFor();<br /> if (proc.exitValue()==0){<br /> proc=rt.exec(commandZip);<br /> proc.waitFor();<br /> if(proc.exitValue()==0){<br /> proc=rt.exec(commandRm);<br /> proc.waitFor();<br /> }<br /> }<br /> exitValue=proc.exitValue();<br /> <br /> } catch (IOException e) {<br /> error(&quot;Не удалось сделать бэкап таблицы &quot; + tableName+&quot;; Exeption : &quot;+e.toString());<br /> } catch (InterruptedException e) {<br /> error(&quot;Не удалось сделать бэкап таблицы &quot; + tableName+&quot;; Exeption : &quot;+e.toString());<br /> }<br /> }<br /> if((backup==1&amp;&amp;exitValue==0)||(backup==0)){<br /> try {<br /> Statement st = connectionSet.getConnection().createStatement();<br /> st.executeUpdate(trorDrop+&quot; &quot; + tableName);<br /> st.close();<br /> }catch (SQLException e){<br /> error(e.getMessage());<br /> }<br /> }<br /> <br /> }<br /> report.append(trorDrop).append(&quot; &quot;).append(tableName).append(&quot;\n&quot;);<br /> }<br /> }<br /> private boolean tableExists(Connection con, String tableName){<br /> boolean result=false;<br /> try {<br /> PreparedStatement ps = con.prepareStatement(&quot;SHOW TABLES LIKE ?&quot;);<br /> ps.setString(1, tableName);<br /> ResultSet rs = ps.executeQuery();<br /> result = rs.next();<br /> rs.close();<br /> ps.close();<br /> } catch (SQLException e) {<br /> e.printStackTrace();<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> Пример конфига скрипта:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> #Скрипт удаления старых таблиц DropOldTables<br /> script.droptables.debug=0<br /> #уведомление на email со списком удалённых таблиц<br /> script.droptables.email=admin@provider.ru<br /> #храним бэкапы<br /> script.droptables.dirbackup=/mnt/backup/bgtables<br /> script.droptables.dumpstring=mysqldump -uroot -p12345 -P 3306 -h 127.0.0.1 bgbilling<br /> script.droptables.month.1.prefix=log_server_1<br /> script.droptables.month.1.months=6<br /> script.droptables.month.2.prefix=log_error_1<br /> script.droptables.month.2.months=6<br /> script.droptables.month.3.prefix=log_function_process<br /> script.droptables.month.3.months=6<br /> script.droptables.month.4.prefix=connection_log_entry_28<br /> script.droptables.month.4.months=6<br /> script.droptables.month.5.prefix=inet_auth_error_28<br /> script.droptables.month.5.months=6<br /> script.droptables.month.6.prefix=log_gscript_process<br /> script.droptables.month.6.months=6<br /> script.droptables.month.6.drop=1<br /> script.droptables.month.7.prefix=npay_add_cost_detail_16<br /> script.droptables.month.7.months=6<br /> script.droptables.month.7.drop=1<br /> script.droptables.month.8.prefix=npay_detail_16<br /> script.droptables.month.8.months=24<br /> script.droptables.month.8.drop=1<br /> script.droptables.month.9.prefix=source_data<br /> script.droptables.month.9.months=9<br /> script.droptables.month.9.drop=1<br /> script.droptables.month.10.prefix=tariff_detail_1<br /> script.droptables.month.10.months=6<br /> script.droptables.month.10.drop=1<br /> script.droptables.month.11.prefix=bgs_query_log<br /> script.droptables.month.11.months=12<br /> script.droptables.month.11.drop=1<br /> script.droptables.month.11.backup=1<br /> script.droptables.month.12.prefix=web_query_log<br /> script.droptables.month.12.months=6<br /> script.droptables.month.12.drop=1<br /> script.droptables.month.12.backup=1<br /> script.droptables.day.1.prefix=data_log_18<br /> script.droptables.day.1.months=3<br /> script.droptables.day.1.drop=1<br /> script.droptables.optimize.month.1.prefix=log_session_18<br /> script.droptables.optimize.month.1.months=2<br /> script.droptables.optimize.month.2.prefix=log_session_29<br /> script.droptables.optimize.month.2.months=2<br /> script.droptables.dialup.cleanup.1.mid=21<br /> script.droptables.dialup.cleanup.1.months=37<br /> script.droptables.dialup.cleanup.1.depth=1<br /> #<br /> &lt;/source&gt;<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 07:40, 1 августа 2018 (UTC)</div> Wed, 01 Aug 2018 07:40:12 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%93%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82_%D0%B4%D0%BB%D1%8F_%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D1%81%D1%82%D0%B0%D1%80%D1%8B%D1%85_%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86_2 Глобальный скрипт для удаления старых таблиц 2 http://wiki.bitel.ru/index.php/%D0%93%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82_%D0%B4%D0%BB%D1%8F_%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D1%81%D1%82%D0%B0%D1%80%D1%8B%D1%85_%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86_2 <p>Cromeshnic:&#32;</p> <hr /> <div>''' Глобальный скрипт для удаления старых таблиц (drop/truncate/dump)'''<br /> <br /> Скрипт удаляет произвольные месячные или дневные таблицы в соответствии с настройками в глобальном конфиге биллинга:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> #Скрипт удаления старых таблиц DropOldTables<br /> script.droptables.debug=0<br /> #уведомление на email со списком удалённых таблиц<br /> script.droptables.email=admin@provider.ru<br /> #храним бэкапы<br /> script.droptables.dirbackup=/mnt/backup/<br /> script.droptables.dumpstring=mysqldump -uroot -pbilling -P 3306 bgbilling<br /> &lt;/source&gt;<br /> <br /> Удаляем (drop=1) или транкейтим (drop=0[default]) месячную таблицу старше 3 месяцев, при необходимости делаем дамп (backup=1):<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.month.1.prefix=log_server_12<br /> script.droptables.month.1.months=3<br /> script.droptables.month.1.drop=0<br /> script.droptables.month.1.backup=0<br /> &lt;/source&gt;<br /> <br /> Удаляем (drop=1) или транкейтим (drop=0[default]) дневную таблицу старше 3 месяцев, при необходимости делаем дамп (backup=1):<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.day.1.prefix=data_log_18<br /> script.droptables.day.1.months=3<br /> script.droptables.day.1.drop=1<br /> script.droptables.day.1.backup=0<br /> &lt;/source&gt;<br /> <br /> Делаем optimize table за 2 месяца, начиная с предыдущего:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.optimize.month.1.prefix=log_session_18<br /> script.droptables.optimize.month.1.months=2<br /> &lt;/source&gt;<br /> <br /> Удаляем записи из session_detail модуля dialup, но только если по ним нет начислений в session_account (удаляем бесплатные трафики). Затем делаем Optimize table. В примере удаляются данные за 1 месяц старше 3 лет:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> script.droptables.dialup.cleanup.1.mid=21<br /> script.droptables.dialup.cleanup.1.months=37<br /> script.droptables.dialup.cleanup.1.depth=1<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.kernel.scripts.global;<br /> <br /> import bitel.billing.server.util.MailMsg;<br /> import ru.bitel.bgbilling.kernel.script.server.dev.GlobalScriptBase;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> <br /> import java.io.IOException;<br /> import java.sql.*;<br /> import java.util.Calendar;<br /> import java.util.Map;<br /> <br /> import static ru.bitel.common.TimeUtils.*;<br /> /**<br /> * Чистим старые таблицы<br /> */<br /> public class DropOldTables<br /> extends GlobalScriptBase<br /> {<br /> @Override<br /> public void execute( Setup setup, ConnectionSet connectionSet )<br /> throws Exception<br /> {<br /> ParameterMap params = setup.sub(&quot;script.droptables.&quot;);<br /> String prefix;<br /> String name;<br /> int months;<br /> int flagDrop = 0;<br /> int backup = 0;<br /> Calendar cal;<br /> StringBuilder report = new StringBuilder();<br /> boolean debug = params.getBoolean(&quot;debug&quot;, false);<br /> String email = params.get(&quot;email&quot;, null);<br /> String backupdir=params.get(&quot;dirbackup&quot;,null);<br /> String dumpstring=params.get(&quot;dumpstring&quot;,null);<br /> <br /> //daily<br /> //Подневные таблицы вида prefix_yyyymmdd<br /> // например, удаляем cdr-логи телефонии старше 6 месяцев (оставляем текущий + 5 предыдущих):<br /> //script.droptables.day.1.prefix=data_log_18<br /> //script.droptables.day.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;day.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> flagDrop = entry.getValue().getInt(&quot;drop&quot;, 0);<br /> backup = entry.getValue().getInt(&quot;backup&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.day.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Удаляем 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=12;i++) {<br /> for (int dd=1; dd&lt;=cal.getActualMaximum(Calendar.DAY_OF_MONTH); dd++){<br /> cal.set(Calendar.DAY_OF_MONTH, dd);<br /> name = prefix + format(cal, &quot;_yyyyMMdd&quot;);<br /> deleteRecord(connectionSet, name, flagDrop, report, backupdir, dumpstring, backup, debug);<br /> }<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> //monthly<br /> //Помесячные таблицы вида prefix_yyyymm<br /> // например, удаляем radius-логи телефонии/dialup старше 6 месяцев (оставляем текущий + 5 предыдущих):<br /> //script.droptables.month.1.prefix=log_server_12<br /> //script.droptables.month.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;month.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> flagDrop = entry.getValue().getInt(&quot;drop&quot;, 0);<br /> backup = entry.getValue().getInt(&quot;backup&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.month.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Удаляем 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=12;i++) {<br /> name = prefix + format(cal, &quot;_yyyyMM&quot;);<br /> deleteRecord(connectionSet, name,flagDrop,report, backupdir, dumpstring, backup, debug);<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> PreparedStatement ps;<br /> <br /> //monthly - optimize tables<br /> //Помесячные таблицы вида prefix_yyyymm<br /> // например, оптимизируем логи телефонии/dialup за 6 месяцев, начиная с предыдущего:<br /> //script.droptables.optimize.month.1.prefix=log_session_18<br /> //script.droptables.optimize.month.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;optimize.month.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.optimize.month.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-1);<br /> //Optimize 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=months;i++) {<br /> name = prefix + format(cal, &quot;_yyyyMM&quot;);<br /> if(tableExists(connectionSet.getConnection(), name)) {<br /> ps = connectionSet.getConnection().prepareStatement(&quot;optimize table &quot;+name);<br /> ps.executeUpdate();<br /> ps.close();<br /> print(&quot;OPTIMIZE TABLE &quot;+name);<br /> report.append(&quot;OPTIMIZE TABLE &quot;).append(name).append(&quot;\n&quot;);<br /> }<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> //monthly - удаляем бесплатные трафики из session_detail_mid_yyyymm модуля dialup<br /> //например, чистим трафики в таблицах старше 3 лет глубиной в 1 месяц и сразу делаем optimize<br /> //script.droptables.dialup.cleanup.1.mid=21<br /> //script.droptables.dialup.cleanup.1.months=36<br /> //script.droptables.dialup.cleanup.1.depth=1<br /> //script.droptables.dialup.cleanup.1.optimize=1<br /> int mid;<br /> int depth;<br /> int optimize;<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;dialup.cleanup.&quot;).entrySet()){<br /> mid = entry.getValue().getInt(&quot;mid&quot;,0 );<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> depth = entry.getValue().getInt(&quot;depth&quot;, 1);<br /> optimize = entry.getValue().getInt(&quot;optimize&quot;, 1);<br /> if(mid&lt;=0 || months&lt;=0){<br /> error(&quot;'mid' or 'months'&lt;=0 for config params script.droptables.dialup.cleanup.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Optimize depth месяцев начиная с now.mm-months<br /> int max_cid;<br /> int min_cid;<br /> int row_count;<br /> int counter;<br /> ResultSet rs;<br /> for(int i=1;i&lt;=depth;i++) {<br /> name = &quot;session_detail_&quot;+mid + format(cal, &quot;_yyyyMM&quot;);<br /> if(tableExists(connectionSet.getConnection(), name)) {<br /> //Будем делать delete кусочками. limit в mysql нельзя использовать в delete с join.<br /> //Поэтому удаляем с отсечкой по индексному полю cid с градацией в 1000 cid-ов<br /> ps = connectionSet.getConnection().prepareStatement(&quot;select max(cid) from &quot;+name);<br /> rs = ps.executeQuery();<br /> max_cid = 0;<br /> if(rs.next()){<br /> max_cid = rs.getInt(1);<br /> }<br /> rs.close();<br /> ps.close();<br /> <br /> ps = connectionSet.getConnection().prepareStatement(&quot;select min(cid) from &quot;+name);<br /> rs = ps.executeQuery();<br /> min_cid = 0;<br /> if(rs.next()){<br /> min_cid = rs.getInt(1);<br /> }<br /> rs.close();<br /> ps.close();<br /> <br /> row_count=0;<br /> counter=0;<br /> for(int id=max_cid-1000; id&gt;min_cid-1000;id-=1000) {<br /> ps = connectionSet.getConnection().prepareStatement(&quot;delete d from &quot; + name + &quot; d &quot; +<br /> &quot;left join session_account_&quot; + mid + format(cal, &quot;_yyyyMM&quot;) + &quot; a on d.session_id=a.session_id and d.sid=a.sid &quot; +<br /> &quot;where a.session_id is null and d.cid&gt;=&quot;+id);<br /> row_count+=ps.executeUpdate();<br /> counter++;<br /> ps.close();<br /> }<br /> print(&quot;* delete ~~~ from TABLE &quot; + name+&quot; [&quot;+row_count+&quot; rows, &quot;+counter+&quot; iterations]&quot;);<br /> report.append(&quot;* delete ~~~ from TABLE &quot;).append(name).append(&quot;\n&quot;);<br /> <br /> if(optimize==1) {<br /> ps = connectionSet.getConnection().prepareStatement(&quot;optimize table &quot; + name);<br /> ps.executeUpdate();<br /> ps.close();<br /> print(&quot;OPTIMIZE TABLE &quot; + name);<br /> report.append(&quot;OPTIMIZE TABLE &quot;).append(name).append(&quot;\n&quot;);<br /> }<br /> }<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> if(email!=null &amp;&amp; !&quot;&quot;.equals(email)){<br /> MailMsg msg = new MailMsg(setup);<br /> msg.sendMessage(email, &quot;Удаление старых таблиц&quot;, report.toString());<br /> }<br /> }<br /> private void deleteRecord(ConnectionSet connectionSet, String tableName, int flagDrop, StringBuilder report, String backupdir, String dumpstring, int backup, boolean debug){<br /> if(tableExists(connectionSet.getConnection(), tableName)) {<br /> String trorDrop=flagDrop==0?&quot;TRUNCATE&quot;:&quot;DROP TABLE&quot;;<br /> String filedm=backupdir+&quot;/&quot;+tableName;<br /> <br /> <br /> print(trorDrop+&quot; &quot; + tableName);<br /> if (!debug){<br /> int exitValue=1;<br /> if(backup==1){<br /> Runtime rt = Runtime.getRuntime();<br /> String commandDump = dumpstring+&quot; &quot;+tableName+&quot; -r &quot;+ filedm+&quot;.sql&quot;;<br /> String commandZip = &quot; zip &quot;+filedm+&quot;.zip &quot;+filedm+&quot;.sql&quot;;<br /> String commandRm = &quot;rm &quot;+filedm+&quot;.sql&quot;;<br /> try {<br /> Process proc=rt.exec(commandDump);<br /> proc.waitFor();<br /> if (proc.exitValue()==0){<br /> proc=rt.exec(commandZip);<br /> proc.waitFor();<br /> if(proc.exitValue()==0){<br /> proc=rt.exec(commandRm);<br /> proc.waitFor();<br /> }<br /> }<br /> exitValue=proc.exitValue();<br /> <br /> } catch (IOException e) {<br /> error(&quot;Не удалось сделать бэкап таблицы &quot; + tableName+&quot;; Exeption : &quot;+e.toString());<br /> } catch (InterruptedException e) {<br /> error(&quot;Не удалось сделать бэкап таблицы &quot; + tableName+&quot;; Exeption : &quot;+e.toString());<br /> }<br /> }<br /> if((backup==1&amp;&amp;exitValue==0)||(backup==0)){<br /> try {<br /> Statement st = connectionSet.getConnection().createStatement();<br /> st.executeUpdate(trorDrop+&quot; &quot; + tableName);<br /> st.close();<br /> }catch (SQLException e){<br /> error(e.getMessage());<br /> }<br /> }<br /> <br /> }<br /> report.append(trorDrop).append(&quot; &quot;).append(tableName).append(&quot;\n&quot;);<br /> }<br /> }<br /> private boolean tableExists(Connection con, String tableName){<br /> boolean result=false;<br /> try {<br /> PreparedStatement ps = con.prepareStatement(&quot;SHOW TABLES LIKE ?&quot;);<br /> ps.setString(1, tableName);<br /> ResultSet rs = ps.executeQuery();<br /> result = rs.next();<br /> rs.close();<br /> ps.close();<br /> } catch (SQLException e) {<br /> e.printStackTrace();<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> Пример конфига скрипта:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> #Скрипт удаления старых таблиц DropOldTables<br /> script.droptables.debug=0<br /> #уведомление на email со списком удалённых таблиц<br /> script.droptables.email=admin@provider.ru<br /> #храним бэкапы<br /> script.droptables.dirbackup=/mnt/backup/bgtables<br /> script.droptables.dumpstring=mysqldump -uroot -p12345 -P 3306 -h 127.0.0.1 bgbilling<br /> script.droptables.month.1.prefix=log_server_1<br /> script.droptables.month.1.months=6<br /> script.droptables.month.2.prefix=log_error_1<br /> script.droptables.month.2.months=6<br /> script.droptables.month.3.prefix=log_function_process<br /> script.droptables.month.3.months=6<br /> script.droptables.month.4.prefix=connection_log_entry_28<br /> script.droptables.month.4.months=6<br /> script.droptables.month.5.prefix=inet_auth_error_28<br /> script.droptables.month.5.months=6<br /> script.droptables.month.6.prefix=log_gscript_process<br /> script.droptables.month.6.months=6<br /> script.droptables.month.6.drop=1<br /> script.droptables.month.7.prefix=npay_add_cost_detail_16<br /> script.droptables.month.7.months=6<br /> script.droptables.month.7.drop=1<br /> script.droptables.month.8.prefix=npay_detail_16<br /> script.droptables.month.8.months=24<br /> script.droptables.month.8.drop=1<br /> script.droptables.month.9.prefix=source_data<br /> script.droptables.month.9.months=9<br /> script.droptables.month.9.drop=1<br /> script.droptables.month.10.prefix=tariff_detail_1<br /> script.droptables.month.10.months=6<br /> script.droptables.month.10.drop=1<br /> script.droptables.month.11.prefix=bgs_query_log<br /> script.droptables.month.11.months=12<br /> script.droptables.month.11.drop=1<br /> script.droptables.month.11.backup=1<br /> script.droptables.month.12.prefix=web_query_log<br /> script.droptables.month.12.months=6<br /> script.droptables.month.12.drop=1<br /> script.droptables.month.12.backup=1<br /> script.droptables.day.1.prefix=data_log_18<br /> script.droptables.day.1.months=3<br /> script.droptables.day.1.drop=1<br /> script.droptables.optimize.month.1.prefix=log_session_18<br /> script.droptables.optimize.month.1.months=2<br /> script.droptables.optimize.month.2.prefix=log_session_29<br /> script.droptables.optimize.month.2.months=2<br /> script.droptables.dialup.cleanup.1.mid=21<br /> script.droptables.dialup.cleanup.1.months=37<br /> script.droptables.dialup.cleanup.1.depth=1<br /> #<br /> &lt;/source&gt;<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 06:33, 13 июля 2018 (UTC)</div> Wed, 01 Aug 2018 07:38:38 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%93%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82_%D0%B4%D0%BB%D1%8F_%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D1%81%D1%82%D0%B0%D1%80%D1%8B%D1%85_%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86_2 Глобальный скрипт для удаления старых таблиц 2 http://wiki.bitel.ru/index.php/%D0%93%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82_%D0%B4%D0%BB%D1%8F_%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D1%81%D1%82%D0%B0%D1%80%D1%8B%D1%85_%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86_2 <p>Cromeshnic:&#32;Новая страница: «''' Глобальный скрипт для удаления старых таблиц (drop/truncate/dump)''' Скрипт удаляет произвольные…»</p> <hr /> <div>''' Глобальный скрипт для удаления старых таблиц (drop/truncate/dump)'''<br /> <br /> Скрипт удаляет произвольные месячный или дневные таблицы в соответствии с настройками в глобальном конфиге биллинга.<br /> <br /> Также опционально может делать бэкап таблицы перед удалением и делать truncate таблицы вместо её удаления.<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.kernel.scripts.global;<br /> <br /> import bitel.billing.server.util.MailMsg;<br /> import ru.bitel.bgbilling.kernel.script.server.dev.GlobalScriptBase;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> <br /> import java.io.IOException;<br /> import java.sql.*;<br /> import java.util.Calendar;<br /> import java.util.Map;<br /> <br /> import static ru.bitel.common.TimeUtils.*;<br /> /**<br /> * Чистим старые таблицы<br /> */<br /> public class DropOldTables<br /> extends GlobalScriptBase<br /> {<br /> @Override<br /> public void execute( Setup setup, ConnectionSet connectionSet )<br /> throws Exception<br /> {<br /> ParameterMap params = setup.sub(&quot;script.droptables.&quot;);<br /> String prefix;<br /> String name;<br /> int months;<br /> int flagDrop = 0;<br /> int backup = 0;<br /> Calendar cal;<br /> StringBuilder report = new StringBuilder();<br /> boolean debug = params.getBoolean(&quot;debug&quot;, false);<br /> String email = params.get(&quot;email&quot;, null);<br /> String backupdir=params.get(&quot;dirbackup&quot;,null);<br /> String dumpstring=params.get(&quot;dumpstring&quot;,null);<br /> <br /> //daily<br /> //Подневные таблицы вида prefix_yyyymmdd<br /> // например, удаляем cdr-логи телефонии старше 6 месяцев (оставляем текущий + 5 предыдущих):<br /> //script.droptables.day.1.prefix=data_log_18<br /> //script.droptables.day.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;day.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> flagDrop = entry.getValue().getInt(&quot;drop&quot;, 0);<br /> backup = entry.getValue().getInt(&quot;backup&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.day.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Удаляем 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=12;i++) {<br /> for (int dd=1; dd&lt;=cal.getActualMaximum(Calendar.DAY_OF_MONTH); dd++){<br /> cal.set(Calendar.DAY_OF_MONTH, dd);<br /> name = prefix + format(cal, &quot;_yyyyMMdd&quot;);<br /> deleteRecord(connectionSet, name, flagDrop, report, backupdir, dumpstring, backup, debug);<br /> }<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> //monthly<br /> //Подмесячные таблицы вида prefix_yyyymm<br /> // например, удаляем cdr-логи телефонии старше 6 месяцев (оставляем текущий + 5 предыдущих):<br /> //script.droptables.month.1.prefix=log_server_12<br /> //script.droptables.month.1.months=6<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; entry: params.subIndexed(&quot;month.&quot;).entrySet()){<br /> prefix = entry.getValue().get(&quot;prefix&quot;);<br /> months = entry.getValue().getInt(&quot;months&quot;, 0);<br /> flagDrop = entry.getValue().getInt(&quot;drop&quot;, 0);<br /> backup = entry.getValue().getInt(&quot;backup&quot;, 0);<br /> if(prefix==null || months&lt;=0){<br /> error(&quot;'prefix' not found or 'months'&lt;=0 for config params script.droptables.month.&quot;+entry.getKey()+&quot;.&quot;);<br /> continue;<br /> }<br /> cal = Calendar.getInstance();<br /> cal.add(Calendar.MONTH,-months);<br /> //Удаляем 12 месяцев начиная с now.mm-months<br /> for(int i=1;i&lt;=12;i++) {<br /> name = prefix + format(cal, &quot;_yyyyMM&quot;);<br /> deleteRecord(connectionSet, name,flagDrop,report, backupdir, dumpstring, backup, debug);<br /> cal.add(Calendar.MONTH,-1);<br /> }<br /> }<br /> <br /> if(email!=null &amp;&amp; !&quot;&quot;.equals(email)){<br /> MailMsg msg = new MailMsg(setup);<br /> msg.sendMessage(email, &quot;Удаление старых таблиц&quot;, report.toString());<br /> }<br /> }<br /> private void deleteRecord(ConnectionSet connectionSet, String tableName, int flagDrop, StringBuilder report, String backupdir, String dumpstring, int backup, boolean debug){<br /> if(tableExists(connectionSet.getConnection(), tableName)) {<br /> String trorDrop=flagDrop==0?&quot;TRUNCATE&quot;:&quot;DROP TABLE&quot;;<br /> String filedm=backupdir+&quot;/&quot;+tableName;<br /> <br /> <br /> print(trorDrop+&quot; &quot; + tableName);<br /> if (!debug){<br /> int exitValue=1;<br /> if(backup==1){<br /> Runtime rt = Runtime.getRuntime();<br /> String commandDump = dumpstring+&quot; &quot;+tableName+&quot; -r &quot;+ filedm+&quot;.sql&quot;;<br /> String commandZip = &quot; zip &quot;+filedm+&quot;.zip &quot;+filedm+&quot;.sql&quot;;<br /> String commandRm = &quot;rm &quot;+filedm+&quot;.sql&quot;;<br /> try {<br /> Process proc=rt.exec(commandDump);<br /> proc.waitFor();<br /> if (proc.exitValue()==0){<br /> proc=rt.exec(commandZip);<br /> proc.waitFor();<br /> if(proc.exitValue()==0){<br /> proc=rt.exec(commandRm);<br /> proc.waitFor();<br /> }<br /> }<br /> exitValue=proc.exitValue();<br /> <br /> } catch (IOException e) {<br /> error(&quot;Не удалось сделать бэкап таблицы &quot; + tableName+&quot;; Exeption : &quot;+e.toString());<br /> } catch (InterruptedException e) {<br /> error(&quot;Не удалось сделать бэкап таблицы &quot; + tableName+&quot;; Exeption : &quot;+e.toString());<br /> }<br /> }<br /> if((backup==1&amp;&amp;exitValue==0)||(backup==0)){<br /> try {<br /> Statement st = connectionSet.getConnection().createStatement();<br /> st.executeUpdate(trorDrop+&quot; &quot; + tableName);<br /> st.close();<br /> }catch (SQLException e){<br /> error(e.getMessage());<br /> }<br /> }<br /> <br /> }<br /> report.append(trorDrop).append(&quot; &quot;).append(tableName).append(&quot;\n&quot;);<br /> }<br /> }<br /> private boolean tableExists(Connection con, String tableName){<br /> boolean result=false;<br /> try {<br /> PreparedStatement ps = con.prepareStatement(&quot;SHOW TABLES LIKE ?&quot;);<br /> ps.setString(1, tableName);<br /> ResultSet rs = ps.executeQuery();<br /> result = rs.next();<br /> rs.close();<br /> ps.close();<br /> } catch (SQLException e) {<br /> e.printStackTrace();<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> Пример конфига скрипта:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> #Скрипт удаления старых таблиц DropOldTables<br /> script.droptables.debug=0<br /> #уведомление на email со списком удалённых таблиц<br /> script.droptables.email=admin@provider.ru<br /> #храним бэкапы<br /> script.droptables.dirbackup=/mnt/backup/bgtables<br /> script.droptables.dumpstring=mysqldump -uroot -p12345 -P 3306 -h 127.0.0.1 bgbilling<br /> script.droptables.month.1.prefix=log_server_1<br /> script.droptables.month.1.months=6<br /> script.droptables.month.2.prefix=log_error_1<br /> script.droptables.month.2.months=6<br /> script.droptables.month.3.prefix=log_function_process<br /> script.droptables.month.3.months=6<br /> script.droptables.month.4.prefix=connection_log_entry_28<br /> script.droptables.month.4.months=6<br /> script.droptables.month.5.prefix=inet_auth_error_28<br /> script.droptables.month.5.months=6<br /> script.droptables.month.6.prefix=log_gscript_process<br /> script.droptables.month.6.months=6<br /> script.droptables.month.6.drop=1<br /> script.droptables.month.7.prefix=npay_add_cost_detail_16<br /> script.droptables.month.7.months=6<br /> script.droptables.month.7.drop=1<br /> script.droptables.month.8.prefix=npay_detail_16<br /> script.droptables.month.8.months=24<br /> script.droptables.month.8.drop=1<br /> script.droptables.month.9.prefix=source_data<br /> script.droptables.month.9.months=9<br /> script.droptables.month.9.drop=1<br /> script.droptables.month.10.prefix=tariff_detail_1<br /> script.droptables.month.10.months=6<br /> script.droptables.month.10.drop=1<br /> script.droptables.month.11.prefix=bgs_query_log<br /> script.droptables.month.11.months=12<br /> script.droptables.month.11.drop=1<br /> script.droptables.month.11.backup=1<br /> script.droptables.month.12.prefix=web_query_log<br /> script.droptables.month.12.months=6<br /> script.droptables.month.12.drop=1<br /> script.droptables.month.12.backup=1<br /> script.droptables.day.13.prefix=data_log_18<br /> script.droptables.day.13.months=3<br /> script.droptables.day.13.drop=1<br /> #<br /> &lt;/source&gt;<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 06:33, 13 июля 2018 (UTC)</div> Fri, 13 Jul 2018 06:33:05 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%93%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82_%D0%B4%D0%BB%D1%8F_%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D1%81%D1%82%D0%B0%D1%80%D1%8B%D1%85_%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86_2 Заглавная страница http://wiki.bitel.ru/index.php/%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0 <p>Cromeshnic:&#32;/* Глобальные скрипты */</p> <hr /> <div>== О BiTel Wiki ==<br /> Здесь вы можете получить больше информации о продуктах BiTel: BGBilling, BGCRM, а также поделиться своим опытом с другими пользователями. В то время как документация часто предоставляет общие сведения о системе и ее настройках, в WiKi приводятся конкретные примеры.<br /> <br /> * &lt;b&gt;[[Как выложить статью на WiKi]]&lt;/b&gt;<br /> * [[Оформление статей]]<br /> * [http://forum.bitel.ru/ Форум BiTel]<br /> <br /> == Специалисты ==<br /> Уважаемые &quot;продвинутые пользователи&quot;. Здесь вы можете располагать записи со своими контактами для оказания воздмездной или безвозмездной помощи по настройке системы BGBilling пользователям, не столь далеко продвинувшимся. Желательно указывать ваши контактные данные и &quot;специализацию&quot;. Отзывы по исполнителям можно оставить/почитать [http://forum.bitel.ru/viewtopic.php?t=9329 на форуме].<br /> {| border=&quot;1&quot; cellpadding=&quot;2&quot; cellspacing=&quot;0&quot;<br /> |- valign=top align=&quot;center&quot; bgcolor=&quot;#eeeeee&quot; <br /> | Имя || Специализация || Контакт || Примечания<br /> |-<br /> | Олег Алтынников || Установка, настройка, поддержка биллинга, миграция с других|| E-mail: murano@linkray.ru, ICQ: 462851472 || Написание скриптов, интеграция с любым оборудованием (IPTV, Интернет, DPI, Телефония), проекты любой сложности под любые задачи, автоматизация, оптимизация работы, Linux/FreeBSD и многое другое<br /> |-<br /> |-<br /> | Рустам Тазуркаев || Mikrotik, переход с NetUp, CISCO || [[Изображение:Cpec_2_contact.png]] &lt;!-- ICQ: 648986--&gt; ||<br /> |-<br /> | Михаил Чернобаев || Скрипты биллинга, Java-расширения, интеграция с другими системами || e-mail: mstr.box@gmail.com || Автоматизация бизнес-процессов: скриптование в биллинге, расширение BGBS API, разработка инструментов интеграции с другими системами. Переход на BGBilling.<br /> |- <br /> | Борис Близнюков || Скрипты биллинга, CISCO, Voip, Mera || [[Изображение:Cpec_4_contact.png]] &lt;!--ICQ: 1996944--&gt; || Только бесплатные краткие консультации. Очень хороший специалист по CISCO.<br /> |-<br /> | Андрей Бехтерев || Cisco, UNIX, ISP, Asterisk || ICQ: 7021464 WEB: http://behterev.su/ || Обширный спектр оборудования. Консалтинг.<br /> |-<br /> | Гершевич М.М. || Доработка конфигурации 1С и прочего ПО. || Тел. +79248454888 +7-(4162)-238-777 WEB: http://www.amurimpulse.ru/ mail: mike1008@mail.ru || Консалтинг. Информационная безопасность. Интеграция биллинга. Крупные проекты. Работа под заказ.<br /> |-<br /> | Андрей Зюзенков || Android, Linux, Eltex, Asterisk, BGBilling || [https://bghelp.ru Сайт], e-mail: info@bghelp.ru || Интеграция, мобильные приложения на Android<br /> |-<br /> | Семён Кошечкин || Java || email/gtalk: [[Файл:Cpec_5_contact.jpg]]|| Скрипты, дополнения, модуль Inet. [[%D0%A1%D0%BB%D1%83%D0%B6%D0%B5%D0%B1%D0%BD%D0%B0%D1%8F:Contributions/Cromeshnic|wiki]]<br /> |-<br /> |-<br /> | Конференция BGBilling || Вопросы касаемо системы BGBilling || https://t.me/bgbilling || Администраторы АСР BGBillig (иногда и разработчики) помогают друг другу в разных вопросах.<br /> |}<br /> <br /> == BGBilling ==<br /> === Установка ===<br /> * [[Установка на gentoo]]<br /> * [[Установка на Sun Solaris]]<br /> * [[Установка на Slackware]]<br /> * [[Установка на FreeBSD]]<br /> * [[Установка на Ubuntu 8 Desktop]]<br /> * [[Установка на Ubuntu 9.10 Desktop]]<br /> <br /> <br /> ==== Перенос данных в биллинг ====<br /> *[[Пример конвертера данных из csv-файлов в базу BGBilling]]<br /> *[[Пример конвертера данных из CSV-файлов в базу BGBilling - 2]]<br /> *[[Пример конвертера данных из CSV-файлов в базу BGBilling - 3]]<br /> *[[Конвертер базы Netup]]<br /> *[[Примеры конвертеров данных из других биллинговых систем]]<br /> <br /> === Администрирование ===<br /> * [[Разграничение прав действий]]<br /> * [[Настройка безопасности сервера биллинга и компонентов биллинга]]<br /> * [[Принудительный останов процессов биллинга]]<br /> * [[Использование подписанного SSL сертификата]]<br /> * [[Запуск scheduler и data_loader с другими портами управления]]<br /> * [[bg-snmp-management|Мониторинг java-процессов по snmp]]<br /> * [[Пример юнита для systemd]]<br /> * [[Скрипты автостарта демонов bgbilling для Debian]]<br /> * [[javaws|Запуск BGBillingClient через Java Web Start]]<br /> * [[Мониторинг Inet-Radius через JMX]]<br /> * [[Интеграция существующего сертификата и приватного ключа SSL в хранилище keystore]]<br /> * [[Адреса платежных шлюзов, для тех кто хочет без доступа в интернет произвести оплату]]<br /> <br /> === Настройка вспомогательного ПО ===<br /> *[[Проксирование обращений к BGBillingServer посредством nginx]]<br /> <br /> ==== WildFly личный кабинет ====<br /> *[[Включение https]]<br /> *[[Смена Тарифного плана в Личном кабинете]]<br /> <br /> ==== MySQL ====<br /> *[[Рекомендации по настройке MySQL]]<br /> *[[database backup|Backup MySQL базы с помощью snapshot'ов (Linux, LVM)]]<br /> *[[Настройка MySQL репликации]]<br /> *[[Установка триггера в MySQL для отслеживания изменений]]<br /> *[[Скрипт восстановления MySQL репликации]]<br /> *[[Simple DB backup]]<br /> *[[Galera]]<br /> <br /> ==== NetFlow ====<br /> *[[Настройка NetFlow-агента IPCAD]]<br /> *[[Разделение NetFlow-потоков]]<br /> <br /> === Технологии ===<br /> *[[BGBilling_XSLT|Всё про XSLT в биллинге]]<br /> <br /> === Разработка ===<br /> *[[Отладка action'ов в IntelliJ IDEA]]<br /> *[[Разработка динамического кода в IDE Eclipse]]<br /> <br /> === [[XSLT]] шаблоны ===<br /> *[[Добавление параметров договора на страницу личного кабинета]]<br /> &lt;!-- *[[Красивые графики статистики в модуле IPN]] --&gt;<br /> *[[Карточки договора]]<br /> *[[Создание XSLT/FO шаблона со штрихкодами]]<br /> *[[Подстановка данных в зависимости от текущего пользователя биллинга]]<br /> *[[Генерация прайса модуля IP телефонии в карточке договора]]<br /> <br /> ==== Счета ====<br /> *[[Печать счета-фактуры и акта на отдельных листах]]<br /> *[[Расширенные счета модуля бухгалтерии]]<br /> *[[Квитанция телефонии физ. лицам]]<br /> *[[Шаблоны вывода названия месяца]]<br /> *[[Изменения в шаблоне в зависимости от месяца документа]]<br /> *[[Добавление новых шрифтов в FO шаблоны]]<br /> <br /> === Интеграция с внешними системами ===<br /> * [[Прямая интеграция с платежными терминалами ЭСФОР / SFOUR]]<br /> * [[Интеграция с платежной системой с использованием модуля Card]]<br /> * [[Интеграция с платежной системой Robokassa]]<br /> * [[SMS рассылка через SMPP]]<br /> * [[SMS рассылка через SMPP по средствам дин кода в 5.2]]<br /> * [[Система учета &quot;Заявки и Наряды&quot; на java]]<br /> * [[Bash скрипт-отсылка смс через телефон при отсутствие ping на заданный узел]]<br /> * [[Запросы в личный кабинет пользователя сторонними системами]]<br /> * [[Запросы к серверу биллинга сторонними системами]]<br /> <br /> ==== 1С ====<br /> * [[BGBilling-1C]]<br /> * [[amurimpulse.ru bgbilling]]<br /> * [[Integrator 1C-BGBilling]]<br /> * [[Пример обращения к биллингу из 1С v.7.7]]<br /> * [[Пример интеграции с 1С v.7.7]]<br /> * [[Пример интеграции с 1С v.8.1]]<br /> * [[Пример интеграции с 1С через custom API]]<br /> * [[Установка unload_status счета через HTTP-запрос]]<br /> ==== Android ====<br /> *[[Разработка мобильных приложений на Android]]<br /> ==== Telegram ====<br /> *[[Разработка telegram ботов]]<br /> <br /> === Динамический код (скрипты BGBS для старых версий) ===<br /> *[[Логгирование в скриптах поведения]]<br /> <br /> ==== Динамический код ====<br /> *[[Конвертирование адреса]]<br /> *[[Глобальная синхронзация услуг модуля npay с тарифным планом]]<br /> *[[Новый номер договора группе договоров]]<br /> *[[Скрипт проверки таймзон (timezone, tz, tzdata) в java]]<br /> <br /> ==== Комплексные решения ====<br /> *[[Предоставление тестового периода пользования услугой]]<br /> *[[Организация системы отслеживания и отключения КТВ должников на BGBS с использованием CRM плагина]]<br /> *[[Автоматизация подключений VPN-клиентов с использованием CRM плагина]]<br /> *[[Пример автоматизации подключения новых клиентов]]<br /> <br /> ==== Глобальные скрипты ====<br /> *[[Скрипт глобальный отмены перехода на тарифы при неоплате]]<br /> *[[Скрипт предоставление скидки пенсионерам]]<br /> *[[Скрипт создания субдоговоров по шаблону]]<br /> *[[Глобальное событие запуска сервера]]<br /> *[[Перемещение в группу через 3 месяца если не было движения денег в наработке]]<br /> *[[Поиск и изменение статусов у договоров]]<br /> *[[Получение списка доступных действий в SQL]]<br /> *[[Глобальный скрипт для удаления старых таблиц]]<br /> *[[Глобальный скрипт для удаления старых таблиц 2]]<br /> <br /> ==== Пользовательские библиотеки скриптов ====<br /> *[[Пересчеты и бонусы]]<br /> *[[Архивирование логов netflow и radius accaunting]]<br /> <br /> ==== Ядро ====<br /> *[[Смена тарифного плана по заданию пользователя]]<br /> *[[Валидация текстового параметра]]<br /> *[[Проверка ИНН/КПП при вводе]]<br /> *[[Проверка параметра договора перед изменением]]<br /> *[[Обработка смены параметра договора]]<br /> *[[Создание списка дополнительных действий для договора]]<br /> *[[Обработка события создания договора]]<br /> *[[Обработка события &quot;добавление услуги RSCM в договор&quot; . Скипт сменяет тариф, подключает абонплату ]]<br /> *[[Приостановление договора клиентом через WEB]]<br /> *[[Скрипт проверки баланса и отключения договора]]<br /> *[[Изменение стандартной логики перетирания статусов]]<br /> *[[Пример продажи OEM ключей с помощью скрипта]]<br /> *[[Пример копирования тарифного плана]]<br /> *[[Получение текущего пользователя биллинга]]<br /> *[[Запуск скрипта до и после акшена]]<br /> *[[Примеры скриптов до и после акшена]]<br /> *[[Примеры динамического кода акшена и веб-сервисов]]<br /> *[[Начисление бонусов на счет при платежах определенного типа]]<br /> *[[Включение должников по приходу платежа]]<br /> *[[Снижение лимита при внесении расхода]]<br /> *[[Изменение суммы лимита определенной группе договоров]]<br /> *[[Синхронизация услуг договора в соответствии с тарифными планами]]<br /> *[[Добавление группы и снятие в зависимости от статуса]]<br /> *[[Управление статусом договора по состоянию баланса]]<br /> *[[Запрет на вход в личный кабинет с закрытых договоров]]<br /> *[[Переход на понижающий тариф только со следующего месяца]]<br /> *[[Пример создания своего интерфейса в клиенте]]<br /> *[[Метки услуг]]<br /> *[[Сравнение прав пользователей]]<br /> *[[Свой список шаблонов договоров]]<br /> *[[Модификация приходящего платежа (снятие процента)]]<br /> *[[Подкрашивание договоров в поиске]]<br /> *[[Пример добавления пунктов в ЛК редиректящих на url]]<br /> *[[Фиксированные суммы для оплаты в старом ЛК]]<br /> <br /> ==== Модуль Bill ====<br /> *[[Создание счета в модуле Bill]]<br /> *[[Создание счета из суммы платежей по классу договоров]]<br /> *[[Создание счета по таблице позиций]]<br /> *[[Создание счета и счет-фактур в модуле Bill(выполнение тех же действий что и руками)]]<br /> *[[Создание счетов на предоплату]]<br /> *[[Распечатка счетов в pdf по событию генерации счета]]<br /> *[[Внешняя программа на JAVA для синхронизации номеров счетов и актов выполненных работ (версия BGBilling 5.0)]]<br /> *[[Автоматическая отправка счетов через глобальный скрипт поведения]]<br /> <br /> ==== Модуль DialUp ====<br /> *[[Запуск переначисления в модуле DialUp]]<br /> *[[Передача ACCEPT вместо REJECT вместе с доп. аттрибутами]]<br /> *[[Обработка запроса учетного периода]]<br /> *[[Переинициализация тарифа в пределах сессии | Обработка запроса учетного периода (переинициализация тарифа в пределах сессии) ]]<br /> *[[Ограничение доступа для различных групп пользователей для BGRadiusDialup]]<br /> *[[Детальное информирование абонентов о причинах ошибки 691]]<br /> *[[Аутентификация с учетом Calling-Id-Station]]<br /> *[[Доп. действие сброса активных соединений]]<br /> *[[Открытие абонплаты по первой установке соединения]]<br /> *[[Пересчет трафика по данным Radius (при потерянных Netflow-логах)]]<br /> *[[Отключение Fake сессий при приходе платежа]]<br /> *[[Ограничение доступа на основе объектов]]<br /> <br /> ==== Модуль DialUp / Cкрипты предобработки RADIUS запросов ====<br /> * [[Уcтановка услуги типа &quot;Время&quot; для BGRadiusDialup]]<br /> * [[Установка фиксированного пароля]]<br /> * [[Нормализация параметра Acct-Session-Id у маршрутизатора Cisco]]<br /> * [[Разделение атрибута User-Name на логин и пароль]]<br /> * [[Вынос MAC адреса из cisco-avp-pair в Calling-Station-Id]]<br /> * [[Копирование Тunnel-Client-Endpoint/Tunnel-Server-Endpoint в Calling-Station-Id/Called-Station-Id]]<br /> * [[Замена radius-атрибутов при авторизации]]<br /> <br /> ==== Модуль Inet / Cкрипты предобработки RADIUS запросов ====<br /> * [[Вынос MAC адреса из cisco-avp-pair в Calling-Station-Id для модуля Inet]]<br /> <br /> ==== Модуль СerberСrypt ====<br /> *[[Изменение подписки карты через web (cerbercrypt)]]<br /> *[[Управление подписками через веб (cerbercrypt)]]<br /> *[[Дин.код для синхронизации pairing с внешнего cas]]<br /> *[[Скрипт активации/деактивации карты при добавлении/удалении]]<br /> <br /> ==== Модуль NPay ====<br /> *[[Определение размера абонентской платы]]<br /> *[[Запуск переначисления в модуле NPay]]<br /> *[[Дебетовые абонплаты. Снятие штрафа за разблокировку.]]<br /> *[[Снятие абонентской платы в дебитовых договорах]]<br /> *[[Предварительное уведомление о блокировке по дебетовым абонплатам]]<br /> *[[ Начисление абонплат по схеме 15-15 ]]<br /> *[[Персональные цены для договоров]]<br /> <br /> ==== Модуль Phone ====<br /> *[[При создании поинта модуля Phone добавление в него абонплат]]<br /> *[[Закрытие_телефонных_договоров]]<br /> <br /> ==== Модуль RSCM ====<br /> *[[Запуск переначисления в модуле RSCM]]<br /> *[[Перенос суммы расхода в наработку RSCM модуля]]<br /> <br /> ==== Модуль VoiceIp ====<br /> *[[Определение стоимости звонка VoiceIp]]<br /> <br /> ==== Модуль VoiceIp / Cкрипты предобработки RADIUS запросов ====<br /> * [[Идентификация Voip оператора по подсети (транзит)]]<br /> * [[Установка параметров звонка Voip]]<br /> * [[Установка фиксированного пароля]]<br /> * [[Разделение атрибута User-Name на логин и пароль]]<br /> * [[Замена radius-атрибутов при авторизации]]<br /> <br /> ==== Плагин CRM ====<br /> *[[Обработка выполненных задач в журнале задач]]<br /> *[[Обработка задач по событию ядра &quot;Поступление платежа&quot;, создание новой задачи и изменение существующей]]<br /> *[[Пример получения информации о задаче]]<br /> *[[Уведомления монтажников о новых активных задачах путем отправки SMS XML запросом]]<br /> <br /> ==== Плагин CashCheck ====<br /> *[[Чек: добавление позиции]]<br /> *[[Чек: завершение формирования]]<br /> *[[Примеры скриптов CashCheck]]<br /> *[[Примеры скриптов обработки онлайн-платежей]]<br /> <br /> ==== Плагин Documents ====<br /> *[[Создание копий документа на договорах]]<br /> <br /> ==== Модуль Inet ====<br /> *[[Скрипт активации учетного периода]]<br /> *[[Скрипт закрытия соединений]]<br /> <br /> === Решения для модулей и плагинов ===<br /> <br /> ==== Модуль DialUP ====<br /> *[[Настройка Lucent Ascend MAX6000 в качестве DialUP сервера]]<br /> *[[Настройка Dial-IN сервера FreeBSD PPPD]]<br /> *[[Настройка VPN сервера LINUX PPPD + POPTOP]]<br /> *[[Настройка шейпера в LINUX PPPD]]<br /> *[[Настройка VPN сервера FreeBSD MPD]]<br /> *[[Настройка PPPoE сервера на Cisco-роутере]]<br /> *[[Настройка PPPoE и/или РРТР (VPN) на Mikrotik]]<br /> *[[Проблема с прохождением update пакетов и сброса сессий в Debian и Ubuntu дистрибутивах]]<br /> *[[Настройка Dial-IN Windows RRAS сервера]]<br /> *[[VPN доступ с повременной тарификацией на базе FreeBSD MPD]]<br /> *[[Организация семейства UNLIMIT тарифов на базе FreeBSD MPD]]<br /> *[[Примеры тарифных планов VPN/DialUp]]<br /> *[[Отключение сессий по PoD на CISCO]]<br /> *[[Пример скрипта управления уровнями BGRadiusDialup]]<br /> *[[Настройка cisco с поддеркой ISG]]<br /> *[[Настройка BGBilling c поддеркой ISG]]<br /> *[[Настройка BGBilling с RedBack SmartEdge (PPPOE)]]<br /> <br /> ==== Модуль E-Mail ====<br /> *[[Почтовая система Exim + Cyrus + OpenLDAP на FreeBSD]]<br /> *[[Postfix/MySQL/BGBilling]]<br /> *[[Postfix+dovecot+ldap]]<br /> *[[Postfix+Mysql+Virtual domains]]<br /> <br /> ==== Модуль Inet ====<br /> *[[Inet FAQ]]<br /> *[[Схемы подключения]]<br /> *[[Расширения]]<br /> *[[Конвертеры из IPN в INET]]<br /> *[[Конвертер: логины Dialup в сервисы inet]]<br /> *[[WiFi-портал с активацией по sms ]]<br /> *[[WiFi-портал с оплатой картой через assist ]]<br /> <br /> ==== Модуль IPN ====<br /> *[[IP/VPN]]<br /> *[[Примеры тарифных планов IPN]]<br /> *[[Настройка BGIPNNetflowCollector]]<br /> *[[Методика определения причины отсутствия трафика в отчете договора]]<br /> *[[Связка с flow-tools]]<br /> *[[Экспорт Netflow-данных в формат Nfdump]]<br /> *[[Реалиазация шлюза на Cisco]]<br /> *[[Реализация шлюзов на BeanShell,примеры стандартных и других шлюзов]] (Manad, Cisco, Zyxel, Mikrotik)<br /> *[[Изменения в manad для работы с одним pipe на множество IP адресов]]<br /> *[[FreeBSD manad, понимающий изменения правил в тарифах]]<br /> *[[Табличный FreeBSD manad, понимающий изменения правил в тарифах]]<br /> *[[Пример реализации скриптового универсального шлюза]]<br /> *[[Конвертер привязок услуг dialup в привязки ipn]]<br /> *[[Реализация скрипта Manad]]<br /> *[[Настройка шлюза Mikrotik]]<br /> *[[Обновление номеров интерфейсов при замене роутера]]<br /> <br /> ==== Модуль Phone ====<br /> * [[Конвертация и загрузка тарифов Телефонии в биллинг]]<br /> * [[Примеры тарифных планов Телефонии]]<br /> * [[Примеры реализации конверторов логов]]<br /> * [[Генератор отчётности для Совинтел]]<br /> <br /> ==== Модуль Reports ====<br /> *[[Редактирование отчетов в iReport]]<br /> *[[Примеры отчётов]]<br /> *[[Использование отчётов для организации универсального поиска]]<br /> *[[Табличные отчёты с динамическими столбцами]]<br /> *[[Табличные отчёты в динамическом коде]]<br /> *[[Сохранение JasperReports-отчёта на сервере в pdf]]<br /> <br /> ==== Модуль TV ====<br /> *[[Активация/добавление модуля на договор через дополнительное действие]]<br /> <br /> ==== Модуль VoiceIP ====<br /> *[[Интеграция Asterisk и BGBilling (Accounting) посредством скрипта предобработки запросов Radius]]<br /> *[[Интеграция Asterisk и BGBilling (Accounting) посредством изменения программного кода Asterisk]]<br /> *[[Интеграция c MVTS]]<br /> *[[Интеграция c Cisco Call Manager Express (CME)]]<br /> *[[Карточная IVR система на базе Cisco]]<br /> *[[Примеры IVR скриптов для Cisco]]<br /> *[[Пример настройки Cisco AS5350]]<br /> *[[Продажа пакетов минут на направления]]<br /> <br /> ==== Плагин Dispatch ====<br /> *[[Импорт старой схемы рассылок баланса в Dispatch]]<br /> <br /> === SQL-запросы ===<br /> *[[Схема связки таблиц тарифов]]<br /> *[[Разные SQL-запросы]]<br /> *[[SQL-запрос: кто сколько платит на каждом тарифе]]<br /> *[[Получение цен тарифов]]<br /> *[[Работа с группами, битовые маски]]<br /> *[[наработка по абонентке и услугам за месяц]]<br /> <br /> ==== CerberCrypt ====<br /> *[[Модуль CerberCrypt: Разные SQL-запросы]]<br /> *[[Модуль CerberCrypt: Поиск битых SQL-связей]]<br /> <br /> === Веб-Интерфейс ===<br /> *[[Свой action в личном кабинете]]<br /> **[[WebAction_CustomSuspend]] - управление статусом договора (v5.0)<br /> *[[Изменение параметров договора из личного кабинета]]<br /> *[[Как убрать ненужные действия в web]]<br /> <br /> === Протоколы ===<br /> *[[Протокол дилерский платежей]]<br /> *[[Протоколы, поддержанные в модуле MPS]]<br /> *[[Протоколы, поддержанные в модуле Phone]]<br /> *[[Медиа: Enaza.zip]]<br /> *[[Медиа: Payonline.zip]]<br /> <br /> === FAQ ===<br /> * [[Не запускается служба под Windows (BGBillingServer, BGCashcheckServer итд)]]<br /> * [[Вопросы вместо русских букв]]<br /> * [[Что происходит с пользователями при рестарте сервера биллинга и BGRadiusDialup]]<br /> * [[Тарификация максимального трафика]]<br /> * [[Field ... doesn't have a default value ]]<br /> * [[Character set ‘cp1251' is not a compiled character set and is not specified in the ‘C:\mysql\\share\charsets\Index.xml’ file ]]<br /> * [[com.mysql.jdbc.exceptions.MySQLSyntaxErrorException: Unknown database 'bgbilling' ]]<br /> * [[Договор не отображается в поиске]]<br /> * [[PPPD проблема с сессиями больше 4ГБ]]<br /> * [[Меню личного кабинета]]<br /> * [[Java.lang.NoClassDefFoundError:_javax/xml/bind/DataBindingException|FreeBSD: Java.lang.NoClassDefFoundError: javax/xml/bind/DataBindingException]]<br /> * [[Manad: после некоторого количества договоров начинает передавать данные на биллинг неправильно ]]<br /> * [[Ошибка выполнения скиптов: Undefined argument:]]<br /> * [[Ошибка в логе &quot;Too many open files&quot;]]<br /> * [[Ошибка в логе &quot;java.lang.OutOfMemoryError : unable to create new native Thread&quot;]]<br /> * [[Ошибка в клиенте &quot;Action NOT FOUND!..&quot;]]<br /> * [[Inet FAQ]]<br /> * [[java.lang.NoSuchMethodError]]<br /> * [[Много таблиц npay_add_cost_detail и npay_detail]]<br /> * [[Тормозит клиент BG под Windows]]<br /> * [[Unable to load authentication plugin]]<br /> <br /> == BGCRM ==<br /> <br /> === Настройка вспомогательного ПО ===<br /> * [[Проксирование обращений к BGCRM посредством nginx]]<br /> <br /> === Плагин BGBilling ===<br /> * [[Синхронизация справочников адресов с BGBilling]]<br /> * [[Активация доверительного платежа в привязанном к процессу договоре биллинга]]<br /> * [[Импорт контрагентов из договоров, sql запрос]]<br /> <br /> === Плагин Document ===<br /> * [[Примеры шаблонов для генерации документов]]<br /> * [[Примеры шаблонов для генерации документов (устаревшее)]]<br /> * [[Пустой шаблон]]<br /> <br /> === Плагин Report ===<br /> * [[Примеры отчётов BGCRM]]<br /> <br /> === Комплексные решения ===<br /> *[[Организация отключения должников КТВ]]<br /> *[[Интеграция с Asterisk для обработки входящих звонков]]<br /> <br /> === Интеграция с внешними системами ===<br /> *[[Asterisk - пример обращения от АТС]]<br /> *[[ВКонтакте - пример интеграции]]<br /> <br /> === Примеры динамического кода ===<br /> *[[Проверка уникальности контрагента по ИНН]]<br /> *[[Проверка уникальности контрагента по паспортным данным]]<br /> *[[Переключение статуса процессов по наступлению момента времени]]<br /> *[[Повышение приоритета процессов]]<br /> *[[Проверка правки параметра процесса]]<br /> *[[Изменение описания процесса по правке параметра]]<br /> *[[Обработка событий процесса согласования]]<br /> *[[Генерация новостей исполнителям при изменении процессов]]<br /> *[[Уведомление на email]]<br /> <br /> === Примеры JEXL конфигураций, скриптов обработки событий ===<br /> *[[Простая обработка событий процесса]]<br /> <br /> == DBInfo ==<br /> * [[Описание программы]]<br /> * [[Установка и настройка программы]]<br /> * [[Исходный код программы]]<br /> <br /> == Разработка ПО ==<br /> В данном разделе собираются рекомендации по разработке ПО. Это накопленная годами и пополняемая база знания призвана упростить обучение в первую очередь разработчиков, работающих с применяемыми в BiTel технологиями: Java, Web (JS, HTML), СУБД MySQL, LINUX, GIT. И разрабатывающих схожие приложения: тиражируемые продукты для автоматизации процессов организаций. Всё предельно конкретно, поэтому большая часть примеров будет приведена на Java.<br /> Однако значительная часть описываемых проблем и принципов довольно фундоментальна и может быть полезна разработчиками в иных областях.<br /> <br /> === В общем ===<br /> * [[Разработка]]<br /> * [[Оптимизация]]<br /> * [[Логирование]]<br /> * [[Сборка и публикация проекта]]<br /> <br /> === Java разработка ===<br /> * [[Работа с git в Eclipse(EGit)]]<br /> * [[Выявление неисправностей приложений]]<br /> * [[Обращение к Web-сервису]]<br /> * [[Работа с SQL в Java]]<br /> * [[Встроенный Application сервер в приложении]]<br /> * [[Потоки в Java]]<br /> * [[Обработка ошибок]]<br /> <br /> ==== Полезные Java библиотеки ====<br /> {| border=&quot;1&quot; cellpadding=&quot;2&quot; cellspacing=&quot;0&quot;<br /> |- valign=top align=&quot;center&quot; bgcolor=&quot;#eeeeee&quot; <br /> | Наименование || Область применения<br /> |-<br /> | [[Jimi - обработка изображений | JIMI]] || Обработка изображений<br /> |}<br /> <br /> === Технологии, используемые в проектах ===<br /> * [[XML]]<br /> * [[XSLT]]<br /> * [[FO(P)]]<br /> * [[REGEXP]]<br /> * [[MySQL REGEXP]]<br /> <br /> === Вспомогательные технологии ===<br /> * [[Сборщик Apache ANT]]<br /> * [[Сбор и анализ сетевого трафика]]<br /> * [[SSH]]<br /> * [[Оптимизация запросов в MySQL]]<br /> <br /> === Требования BiTel к оформлению ===<br /> * [[Java кода]]<br /> * [[MySQL кода]]</div> Fri, 13 Jul 2018 06:19:43 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0 Vlan per user + Cisco IP subscriber interface + ISG http://wiki.bitel.ru/index.php/Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG <p>Cromeshnic:&#32;</p> <hr /> <div>= Описание задачи =<br /> Клиентам предоставляется доступ по схеме vlan per user на роутерах cisco.<br /> Необходимо реализовать учёт и управление услугами в модуле Inet.<br /> <br /> = Решение =<br /> Сразу оговоримся, что в нашем случае речь идёт об услуге '''MPLS IP VPN'''.<br /> Для доступа интернет, возможно, появятся какие-то нюансы, но в целом механизм тот же.<br /> <br /> Схема решения такая:<br /> *на клиентском интерфейсе настраивается ip subscriber interface + isg<br /> *интерфейсы авторизуются в BGInetAccess по паре &quot;устройство+интерфейс&quot;<br /> *Настройки скорости выдаются через ISG-сервисы, смена тарифа происходит через CoA<br /> *Трафик собираем по радиус-счётчикам ISG-сервисов и родительской сессии, либо через Netflow<br /> *В сервисе модуля Inet указываем только устройство и интерфейс<br /> <br /> В нашем случае в биллинге не задаются ни сеть IP клиента на интерфейсе, ни его VLAN, т.к. для тарификации они не играют роли.<br /> Но это можно реализовать для учёта ресурсов, а также для автоматического конфигурирования.<br /> <br /> ''Дополнительно усложним задачу:<br /> В некоторых случаях клиенту предоставляется VPN + доступ в интернет через NAT на нашей циске.<br /> Доступ в интернет считается в другом модуле, но такая схема несколько усложняет задачу, поскольку тогда в каждой точке VPN-а нужно разделять трафик на собственно VPN и интернет, чтобы не тарифицировать последний 2 раза. Эту схему мы реализуем отдельным типом сервиса с отдельной привязкой трафика и отдельными сервисами ISG.''<br /> <br /> = Настройка биллинга =<br /> == Настройка модуля ==<br /> Для услуг VPN было решено завести отдельный экземпляр модуля Inet с названием 'VPN'.<br /> <br /> Плюсы:<br /> *Можно учитывать услуги интернета и vpn на одном договоре без проблем с пересечением тарифных планов (основная причина, т.к. в наследство осталось много таких договоров)<br /> *Учёт услуг, визуальное разделение услуг на договоре и в отчётах.<br /> <br /> Минусы:<br /> *Дублирование ресурсов между несколькими экземплярами модулей Inet: одни и те же устройства, vlan, интерфейсы используются в разных местах. Могут быть проблемы с учётом.<br /> <br /> Конфигурация модуля:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Активные и приостановленные статусы договора<br /> contract.status.active.codes=0<br /> contract.status.suspend.codes=3,4<br /> # Проверка цены в тарифе: 0 - проверка отсутсвует, 1 - ошибка, только если у сессии есть трафик определенного типа,<br /> # но для него нет цены, 2 - ошибка, если хотя бы для одного типа трафика в привязке типа сервиса нет цены (по умолчанию - 1)<br /> #http://forum.bgbilling.ru/viewtopic.php?p=65629#p65629<br /> accounting.tariffication.checkPrice=0<br /> # Режим активации учетного периода, если не используется скрипт на событие активации,<br /> # 0 (по умолчанию) - активация со дня подключения (старта сессии), 1 - активация с начала месяца.<br /> # Следует учитывать, что учетный период является второй величиной при вычислении пропорциональности<br /> # в тарифной ветке &quot;Диапазон трафика&quot;<br /> #accounting.period.activation.mode=0<br /> <br /> # Нужно ли отключать сервис с типом инициации &quot;по трафику&quot;, если тариф не найден<br /> #serv.disableOnTariffError=0<br /> <br /> #Пункты Web - меню<br /> #web.menuItem1=Отчет по сессиям Inet<br /> #web.menuItem2=Смена пароля на логины Inet<br /> #web.menuItem3=none<br /> #web.menuItem3=Отчет по трафикам Inet<br /> <br /> # Параметры автоматической генерации логина для сервиса.<br /> # Минимальное значение логина при генерации логина<br /> #serv.login.min=1<br /> # Максимальное значение логина при генерации логина (т.е. если в базе присутствуют логины 1,2,3 и 10000000,<br /> # то при генерации создастся логин 4, а не 10000001)<br /> #serv.login.max=9999999<br /> <br /> # Парамерты автоматической генерации пароля для сервиса. Можно указать в конфигурации модуля, конфигурации устройства, конфигурации типа сервиса<br /> # (в последнем случае значения будут главнее):<br /> # Минимальная длина пароля<br /> serv.password.length.min=5<br /> # Максимальная длина пароля<br /> serv.password.length.max=16<br /> # Разрешенные символы (используются также при генерации пароля)<br /> serv.password.chars=1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz<br /> # Описание разрешенных символов, если пользователь ввел другие<br /> serv.password.chars.description=В пароле допустимы только цифры и латинские буквы.<br /> # Длина для автоматически генерируемого пароля<br /> serv.password.length.auto=6<br /> # Используемые символы для автоматически генерируемого пароля (по умолчанию значение берется из параметра serv.password.chars)<br /> #serv.password.chars.auto=<br /> <br /> # Параметры активации карточек модуля card при использовании InetRadiusProcessor,<br /> # данные параметры можно указать как в конфигурации модуля, так и в конфигурации устройства.<br /> # Код модуля card<br /> #card.moduleId=<br /> # id услуг активации<br /> #card.activate.serviceIds=<br /> # Минимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.min=0<br /> # Максимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.max=0<br /> &lt;/source&gt;<br /> <br /> Также не забываем настроить задачу планировщика &quot;Активация/деактивация сервисов по периоду&quot; хотя бы раз в сутки в полночь, чтобы корректно обрабатывалось переоформление и перенос сервисов с договора на договор будущим числом.<br /> <br /> == Типы трафиков и привязки ==<br /> === Типы трафиков ===<br /> <br /> [[Файл:traffic_types.png]]<br /> <br /> === Привязки ===<br /> ==== Radius - full ====<br /> Простая привязка с 2 типами трафика: входящий/исходящий, которые берутся из стандартных счетчиков радиус-пакетов родительской сессии.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> &lt;div class=&quot;collapsible collapsed&quot;&gt;[[Файл:radius-full.png]]&lt;/div&gt;<br /> <br /> ==== Radius - NAT ====<br /> Привязка для VPN + NAT.<br /> Для родительской сессии будет 2 дочерних сессии ISG: IPVPN-NAT-INET и IPVPN-NAT-VPN-xxx, где xxx - скорость.<br /> Будем брать трафики из них.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> [[Файл:radius-nat.png]]<br /> <br /> == Типы сервисов ==<br /> <br /> === VPN-IPoE ===<br /> [[Файл:serv-type-ipoe.png]]<br /> <br /> === VPN-IPoE (+NAT) ===<br /> [[Файл:serv-type-ipoe-nat.png]]<br /> <br /> == Опции ==<br /> Заведём опции для соответствующих ISG-сервисов.<br /> Опции FLOWON/FLOWOFF нужны для включения/выключения netflow на интерфейсе<br /> [[Файл:options.png]]<br /> <br /> == Устройства и ресурсы ==<br /> <br /> === Группы устройств ===<br /> Не используются.<br /> === Типы устройств ===<br /> Мы используем 3 типа устройств:<br /> *Группа (ProcessGroup) - пустой тип устройств. Указывается в качестве рута для BGInetAccess и BGInetAccounting (см соответствующий раздел)<br /> *Город - пустой тип устройства, добавлен для разбиения дерева по городам. В будущем, возможно, к нему будут привязываться специфические ProcessHandler-ы, отдельные конфиги или выделяться свои Access и Accounting сервера для каждого города.<br /> *IPoE - тип устройства для цисок с поддержкой ip subscriber interface + ISG<br /> <br /> [[Файл:devicetype-ipoe.png]]<br /> <br /> Конфиг типа устройства IPoE:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Realm default атрибуты<br /> radius.realm.default.attributes=cisco-SSG-Account-Info=ADEFAULT;cisco-avpair=subscriber:accounting-list=BG-DSI-IPVRF<br /> #коды ошибок, которые обрабатываются системой Reject-To-Accept (то же самое, что и realm.reject.error)<br /> #http://bgbilling.ru/v5.2/doc/ch18s20.html<br /> #-------reject-to-accept отсутствует<br /> #radius.disable.accessCodes=4,10,11,12,44<br /> # Какие адреса выдавать при ответе Access-Accept в состоянии disable: <br /> # 0 (по умолчанию) - из radius.disable.ipCategories, 1 - так же, как если бы не было ошибки (в том числе привязанные к сервису в договоре)<br /> #radius.disable.mode=0<br /> # код категории ресурсов Fake пула<br /> #radius.disable.ipCategories=7<br /> # радиус атрибуты, отправляемые в режиме Reject-To-Accept<br /> #radius.disable.attributes=<br /> # Id фиктивного сервиса, к которому будут привязываться сессии, по которым нормальный сервис не был найден (код ошибки: 1, логин не найден).<br /> # Необходим, если в radius.disable.accessCodes присутствует код 1<br /> #radius.disable.servId=<br /> # Атрибуты, при наличии которых соединение должно считаться в состоянии DISABLE (т.е. с ограниченным доступом)<br /> #radius.disable.pattern.attributes=<br /> # Вендор атрибута, где хранится MAC-адрес<br /> # Берём стандартный NAS-Port-Id<br /> radius.macAddress.vendor=-1<br /> # Код атрибута, где хранится MAC-адрес<br /> # Берём NAS-Port-Id<br /> radius.macAddress.type=87<br /> # Префикс атрибута (если есть), где хранится MAC-адрес. Например, для cisco avpair <br /> #radius.macAddress.prefix=<br /> #Порт для отправки PoD и CoA запросов (по умолчанию - порт, заданный в параметрах устройства Хост/порт)<br /> radius.port=1700<br /> #<br /> # Режим поиска сервиса: 0 (по умолчанию) - по логину, 1 - по интерфейсу на устройстве (в предобработке должны быть<br /> # проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или INTERFACE_ID), 2 - по VLAN на устройстве (в предобработке<br /> # должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID), 4 - по VLAN на устройстве или<br /> # дочернем устройстве (в предобработке должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID),<br /> # 5 - по MAC-адресу на устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS), 6 - по MAC-адресу на<br /> # устройстве или дочернем устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS).<br /> radius.servSearchMode=1,0<br /> #<br /> # Нужно ли проверять пароль: 0 - нет, 1 (по умолчанию) - да.<br /> radius.password.verification=0<br /> #<br /> # При выдаче access-accept добавлять запись в базу<br /> # необходимо, если используется reject-to-accept и по старт пакету нельзя определить в каком состоянии соединение<br /> #чтобы Access при Access-Accept добавлял соединение в базе со статусом WAIT и указанием выданного состояния и опций<br /> connection.start.fromAccept=1<br /> # Бывают ситуации, когда start-пакет не дошел до Accounting-сервера. В этом случае, при<br /> # 1 (значение по умолчанию) - сессия создастся от текущего момента,<br /> # 2 - Accounting проверит, что время сессии из update/stop пакета не больше, чем значение connection.close.timeout и создаст сессию от ее начала, иначе,<br /> # если время сессии больше чем connection.close.timeout, сессия создастся от текущего момента,<br /> # 0 - сессия без старт-пакета создана не будет.<br /> connection.start.fromUpdate=1<br /> # таймаут перевода соединения в статус suspended при остутствии радиус пакетов<br /> connection.suspend.timeout=1200<br /> # таймаут закрытия соединения при остутствии радиус пакетов (не складывается с connection.suspend.timeout)<br /> connection.close.timeout=1260<br /> #При завершении соединения по сигналу Stop-пакетом (RADIUS-Stop) оно фактически завершается через количество секунд, определяемое переменной connection.finish.timeout. Это позволяет, в частности, реализовать сбор &quot;запоздалой&quot; информации о трафике, которая может прийти после Stop-пакета.<br /> connection.finish.timeout=2<br /> # Проверка на повторную аутентификацию при Access-Request. Бывает нужна в случаях, когда NAS сбрасывает (теряет) сессию, но<br /> # Stop-пакет не присылает и клиент пытается подключиться повторно, но у него стоит ограничение на максимум одну сессию. При совпадении<br /> # callingStationId с одной из активных сессий и установленным параметром: 1 - осуществляется попытка закрытия старой сессии (connectionClose),<br /> # 2 - попытка закрытия сессии (connectionClose) и завершение ее в базе, не дожидаясь стоп пакета, 3 - завершение в базе.<br /> #radius.connection.checkDuplicate=0<br /> #<br /> # Нужно ли убирать домен перед поиском сервиса по логину из поля User-Name. По умолчанию - да (1).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> #для IPoE какой пришёл, такой и берём, там не должно быть лишнего<br /> radius.username.removeDomain=0<br /> #<br /> # Нужно ли убирать пробелы из поля User-Name перед поиском логина. По умолчанию - нет (0).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> radius.username.removeWhitespace=0<br /> #<br /> # Шаблон вывода ошибки в мониторе с использованием атрибутов из RADIUS-пакета<br /> #radius.accessError.infoPattern=LOGIN:$User-Name<br /> #<br /> # Параметры активации сервисов<br /> # длина паузы, если возникла ошибка<br /> #sa.error.pause=60<br /> # количество заданий за раз<br /> #sa.batch.size=20<br /> # время (сек) ожидания завершения всех заданий (при асинхронной работе)<br /> #sa.batch.wait=5<br /> # пауза (сек) после обработки заданий<br /> #sa.batch.pause=0<br /> # время (сек) ожидания новой задачи перед вызовом disconnect.<br /> #sa.batch.waitNext=5<br /> #<br /> #----------------------------------------<br /> #параметры обработчика активации сервисов<br /> #----------------------------------------<br /> # откуда при отправке CoA брать атрибуты опций (по умолчанию - те же атрибуты, что выдаются при удачной авторизации)<br /> #sa.radius.option.attributesPrefix=radius.inetOption.<br /> #sa.radius.connection.attributes=NAS-Port, Acct-Session-Id, User-Name, Framed-IP-Address, NAS-IP-Address, NAS-Identifier<br /> sa.radius.connection.attributes=Acct-Session-Id, User-Name<br /> #режим отправки CoA. 0 - команды 0xc и 0xb в одном пакете для всех сервисов, 1 - команды 0xc и 0xb в отдельном пакете для каждого сервиса, 2 - атрибуты subscriber:command= в раздельных пакетах для каждого сервиса<br /> sa.radius.connection.coa.mode=1<br /> #Что делать для закрытия соединения:<br /> # 0 (default) - ничего<br /> # 2 - шлём PoD<br /> # 3 - шлём subscriber:command=account-logoff<br /> sa.radius.connection.close.mode=3<br /> #если dhcp lease time большой, а при положительном балансе доступ нужно дать (даже если адрес сейчас выдан серый), нужно установить 1<br /> sa.radius.connection.coa.onEnable=0<br /> #атрибуты CoA запроса для прекращения доступа (используется при sa.radius.connection.withoutBreak=1)<br /> #sa.radius.disable.attributes={@radius.disable.attributes}<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой CoA<br /> #sa.radius.coa.attributes=<br /> #добавлять ли при отправке CoA атрибуты реалма (для default - из radius.realm.default.attributes)<br /> #sa.radius.realm.addAttributes=0<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой PoD<br /> #sa.radius.pod.attributes=<br /> #<br /> #<br /> ###### VPN services ######<br /> radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle<br /> #Сопоставление nas-port-id из запроса с id порта в биллинге по имени интерфейса (см ru.dsi.bgbilling.modules.inet.dyn.device.cisco.ISGIPoEProtocolHandler)<br /> radius.ipoe.nas_port_id.pattern.1.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{1,4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.1.replacement=$1/0/$2/$3<br /> radius.ipoe.nas_port_id.pattern.2.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{8}$)0{0,3}([1-9]\d{0,3})(?=\d{4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.2.replacement=$1/0/$2/$4.$3<br /> radius.ipoe.nas_port_id.pattern.3.pattern=^BD(\d+)$<br /> radius.ipoe.nas_port_id.pattern.3.replacement=255/0/$1<br /> &lt;/source&gt;<br /> <br /> Обратите внимание на строчку:<br /> &lt;source lang=&quot;ini&quot;&gt;radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle&lt;/source&gt;<br /> Здесь мы говорим, что все опции модуля, находящиеся в ветке с id=1 (&quot;Группа: VPN&quot;), следует трактовать как сервисы ISG с соответствующим названием.<br /> <br /> Параметры radius.ipoe.nas_port_id.pattern.* нужны для самописного предобработчика радиус-пакетов ISGIPoEProtocolHandler.<br /> В них определяются regexp-шаблоны, по которым сопоставляется значение радиус-атрибута Nas-Port-Id (например, 0/0/0/1112) и название интерфейса в биллинге (Gi0/0.1112).<br /> Подробнее см. описание класса.<br /> <br /> ==== ServciceActivator ====<br /> Для нашей схемы используется модифицированный ISGServciceActivator.<br /> <br /> Отличия от стандартного Бителовского:<br /> - соответствие &quot;опция - сервис ISG&quot; берётся из optionRadiusAttributesMap, чтобы работали новые шаблоны атрибутов (radius.inetOption.1.template)<br /> - в соответствие &quot;опция - сервис ISG&quot; добавлена зависимость от realm-а<br /> - убрано всё, что касается DHCP<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorEvent;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetConnection;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntimeMap;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> <br /> import java.util.*;<br /> <br /> /**<br /> * Конфигурация устройства:<br /> * sa.radius.connection.coa.mode = 1<br /> * режим отправки CoA:<br /> * 0 - отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> * 1 - (default) отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> * 2 - отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> *<br /> * sa.radius.service.disable =<br /> * имена сервисов, при котором доступ отключен<br /> * отправляются в режиме Reject-To-Accept<br /> * по-умолчанию не указано<br /> *<br /> * sa.radius.connection.close.mode = 2<br /> * что делать для закрытия соединения:<br /> * 1 - ничего не делать<br /> * 2 - (default) посылаем PoD<br /> * 3 - посылаем subscriber:command=account-logoff<br /> *<br /> * sa.radius.connection.close.disableServices = 0<br /> * отключать ли сервисы ISG при закрытии<br /> * 0 - (default) не отключать<br /> * 1 - отключать (посылаем CoA на отключение всех сервисов перед тем как закрыть соединение по sa.radius.connection.close.mode)<br /> */<br /> public class ISGServiceActivator<br /> extends AbstractRadiusServiceActivator<br /> implements ServiceActivator<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGServiceActivator.class );<br /> <br /> /**<br /> * per-realm:<br /> * код опции -&gt; набор сервисов ISG<br /> */<br /> protected Map&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt; optionISGServiceMap = new HashMap&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt;();<br /> <br /> /**<br /> * имя(имена) сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND_PACKET = 0;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND = 1;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> */<br /> protected static final int COA_MODE_SUBSCR_COMMAND = 2;<br /> <br /> /**<br /> * Режим отправки команд<br /> */<br /> protected int coaMode;<br /> <br /> @Deprecated<br /> protected static final int CLOSE_MODE_POD_DEPRECATED = 0;<br /> <br /> protected static final int CLOSE_MODE_NONE = 1;<br /> protected static final int CLOSE_MODE_POD = 2;<br /> protected static final int CLOSE_MODE_SUBSCR_COMMAND = 3;<br /> <br /> protected int closeMode;<br /> protected boolean disableServicesOnClose;<br /> <br /> public ISGServiceActivator()<br /> {<br /> super( null, false, &quot;Acct-Session-Id&quot;, false );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object init( Setup setup, int moduleId, InetDevice device, InetDeviceType deviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, device, deviceType, deviceConfig );<br /> <br /> this.coaMode = deviceConfig.getInt( &quot;sa.radius.connection.coa.mode&quot;, deviceConfig.getInt( &quot;radius.coa.mode&quot;, deviceConfig.getInt( &quot;coa.mode&quot;, COA_MODE_SSG_COMMAND ) ) );<br /> <br /> //вендор атрибута cisco-SSG-Account-Info (9)<br /> int ciscoSSGAccountInfo_attribute_vendor=9;<br /> //id атрибута cisco-SSG-Account-Info (250)<br /> int ciscoSSGAccountInfo_attribute_id=250;<br /> <br /> Map&lt;Integer, Set&lt;String&gt;&gt; map;<br /> Set&lt;String&gt; set;<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; raList;<br /> InetOptionRuntimeMap inetOptionRuntimeMap = InetOptionRuntimeMap.getInstance(moduleId);<br /> // определение сервисов на каждой из опций<br /> for(Map.Entry&lt;String, Map&lt;Integer, RadiusAttributeSet&gt;&gt; e_realm : this.optionRadiusAttributesMap.getRealmMap().entrySet()){<br /> <br /> map = this.optionISGServiceMap.get(e_realm.getKey());<br /> if(null==map){<br /> map = new HashMap&lt;Integer, Set&lt;String&gt;&gt;();<br /> this.optionISGServiceMap.put(e_realm.getKey(), map);<br /> }<br /> //Перебираем опции в realm-е<br /> for(Map.Entry&lt;Integer, RadiusAttributeSet&gt; e_option : e_realm.getValue().entrySet()){<br /> logger.info(&quot;option = &quot;+inetOptionRuntimeMap.get(e_option.getKey()).title+&quot;(&quot;+e_option.getKey()+&quot;), realm = &quot;+e_realm.getKey()+&quot;, ra = &quot;+e_option.getValue());<br /> set = null;<br /> raList = e_option.getValue().getAttributes(ciscoSSGAccountInfo_attribute_vendor, ciscoSSGAccountInfo_attribute_id);<br /> if(raList!=null){<br /> for(RadiusAttribute&lt;?&gt; attr : raList){<br /> if(null==set){<br /> set = new HashSet&lt;String&gt;();<br /> }<br /> //вырезаем из атрибута cisco-SSG-Account-Info=ASERVICENAME имя сервиса SERVICENAME<br /> set.add(attr.getValue().toString().substring(1));<br /> }<br /> if(set!=null &amp;&amp; set.size()&gt;0){<br /> map.put(e_option.getKey(), set);<br /> }<br /> }<br /> }<br /> }<br /> <br /> // сервис(ы), отправляемый в режиме Reject-To-Accept<br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;sa.radius.service.disable&quot;, deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> <br /> logger.info( &quot;Disable services: &quot; + disableServiceNames );<br /> <br /> this.closeMode = deviceConfig.getInt( &quot;sa.radius.connection.close.mode&quot;, CLOSE_MODE_POD );<br /> this.disableServicesOnClose = deviceConfig.getInt( &quot;sa.radius.connection.close.disableServices&quot;, 0 ) &gt; 0;<br /> <br /> return null;<br /> }<br /> <br /> /**<br /> *<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionModify( ServiceActivatorEvent e )//TODO добавить timeout, чтобы не отправлять слишком быстро. Дожидаться ответов например.<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection modify: oldState: &quot; + e.getOldState() + &quot;; newState: &quot; + e.getNewState() + &quot;; oldOptionSet: &quot; + e.getOldOptions() + &quot;; newOptionSet: &quot; + e.getNewOptions() );<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( e.getNewState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> <br /> if( e.getOldState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> // отключаем disable сервис и включаем активные опции<br /> return sendCommands( connection, disableServiceNames, optionsToServiceNames(e.getRealm(), e.getNewOptions()) );<br /> }<br /> <br /> Collection&lt;Integer&gt; removeOptions = e.getOptionsToRemove();<br /> Collection&lt;Integer&gt; addOptions = e.getOptionsToAdd();<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), removeOptions), optionsToServiceNames(e.getRealm(), addOptions ) );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionClose( ServiceActivatorEvent e )<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection close&quot; );<br /> <br /> Object result;<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( disableServicesOnClose )<br /> {<br /> result = sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> else<br /> {<br /> result = null;<br /> }<br /> <br /> switch( closeMode )<br /> {<br /> default:<br /> case CLOSE_MODE_NONE:<br /> {<br /> break;<br /> }<br /> <br /> case CLOSE_MODE_POD_DEPRECATED:<br /> case CLOSE_MODE_POD:<br /> {<br /> RadiusPacket request = radiusClient.createDisconnectRequest();<br /> prepareRequest( request, connection );<br /> <br /> logger.info( &quot;Send PoD: \n&quot; + request );<br /> result = radiusClient.sendAsync( request );<br /> <br /> break;<br /> }<br /> <br /> case CLOSE_MODE_SUBSCR_COMMAND:<br /> {<br /> logger.info( &quot;Connection close (logoff)&quot; );<br /> <br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=account-logoff&quot; ) );<br /> <br /> logger.info( &quot;Send logoff CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected Collection&lt;String&gt; optionsToServiceNames(String realm, final Collection&lt;Integer&gt; options)//, final Collection&lt;String&gt; serviceNames )<br /> {<br /> if( options == null || options.size() == 0 )<br /> {<br /> return null;<br /> }<br /> <br /> if(null==realm || &quot;&quot;.equals(realm)){<br /> realm = &quot;default&quot;;<br /> }<br /> <br /> final Set&lt;String&gt; result = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;( options.size() + 2 ) );<br /> <br /> for( Integer option : options )<br /> {<br /> Set&lt;String&gt; serviceNames = this.optionISGServiceMap.get(realm).get(option);<br /> if( serviceNames == null ){<br /> serviceNames = this.optionISGServiceMap.get(&quot;default&quot;).get(option);<br /> }<br /> if( serviceNames != null )<br /> {<br /> result.addAll( serviceNames );<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> /**<br /> * Отправка команд на деактивацию и активацию сервисов<br /> * @param connection - InetConnection<br /> * @param serviceNamesDeactivate - список сервисов, которые нужно деактивировать<br /> * @param serviceNamesActivate - список сервисов, которые нужно активировать<br /> * @return<br /> * @throws Exception<br /> */<br /> protected Object sendCommands( final InetConnection connection, final Collection&lt;String&gt; serviceNamesDeactivate, final Collection&lt;String&gt; serviceNamesActivate )<br /> throws Exception<br /> {<br /> Object result = null;<br /> <br /> if(logger.isInfoEnabled()){<br /> logger.info(&quot;Sending commands to deactivate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesDeactivate)+&quot;]&quot;);<br /> logger.info(&quot;Sending commands to activate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesActivate)+&quot;]&quot;);<br /> }<br /> <br /> switch( coaMode )<br /> {<br /> case COA_MODE_SSG_COMMAND_PACKET:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send deactivate services CoA:\n&quot; + packet );<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send activate services CoA:\n&quot; + packet );<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SSG_COMMAND:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SUBSCR_COMMAND:<br /> default:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=deactivate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=activate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> ==== ProtocolHandler ====<br /> Используется собственный ProtocolHandler, необходимый для поиска сервисов Inet по порту на основе Nas-Port-Id.<br /> Задача в том, чтобы по данным из радиус-пакета авторизации найти и авторизовать сервис в биллинге.<br /> В пакете приходят:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> User-Name=nas-port:XXX.XXX.XXX.XXX:0/0/0/1112<br /> NAS-Port-Id=0/0/0/1112<br /> &lt;/source&gt;<br /> где <br /> XXX.XXX.XXX.XXX - ip-адрес NAS-а<br /> 0/0/0/1112 - слот/карта/интерфейс/инкапсуляция<br /> Было решено использовать NAS-Port-Id<br /> <br /> Схема следующая:<br /> *При подключении клиента конфигурируем имя интерфейса в соответствии с инкапсуляцией: для 'encapsulation dot1Q XXX' интерфейс будет 'interface GigabitEthernet0/0.XXX'<br /> *Заводим интерфейс на устройстве в биллинге: имя=Gi0/0.XXX. ''При необходимости проставляем нужный ifIndex в поле 'интерфейс', если хотим собирать netflow''<br /> *При загрузке (или перечитывании конфигурации) наш ISGIPoEProtocolHandler парсит параметры конфигурации radius.ipoe.nas_port_id.pattern.*, по которым создаёт в памяти соответствие 'NAS-Port-Id'-&gt;'ifaceId' для каждого заведённого в биллинге интерфейса устройства. Например, '0/0/0/1112'-&gt;'id интерфейса с именем Gi0/0.1112 в биллинге'<br /> *При авторизации ISGIPoEProtocolHandler ищет интерфейс по NAS-Port-Id из пакета и проставляет опцию INTERFACE_ID, по которой стандартный радиус-процессор будет затем искать сервис Inet.<br /> *При изменении интерфейсов устройства в биллинге кэш 'NAS-Port-Id'-&gt;'ifaceId' в ISGIPoEProtocolHandler-е автоматически обновляется<br /> <br /> ''Примечание:<br /> Если используется QinQ, то NAS-Port-Id будет вида 0/0/0/105.2015 для: 'encapsulation dot1Q 2015 second-dot1q 105', а имя интерфейса: 'Gi0/0.20150105'. Последнее настраивается в radius.ipoe.nas_port_id.pattern.*''<br /> <br /> Для 5.2:<br /> &lt;source lang=&quot;java&quot; collapse=&quot;true&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.managed.ServerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.bitel.oss.systems.inventory.resource.common.DeviceInterfaceService;<br /> import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterface;<br /> import ru.bitel.oss.systems.inventory.resource.common.event.DeviceInterfaceModifiedEvent;<br /> <br /> import java.util.HashMap;<br /> import java.util.List;<br /> import java.util.Map;<br /> import java.util.SortedMap;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * ProtocolHandler для работы с cisco ip subscriber interface + cisco ISG<br /> * В предобработке устанавливается опция пакета InetRadiusProcessor.INTERFACE_ID, где указывается номер интерфейса в биллинге,<br /> * соответствующий атрибуту Nas-Port-Id из пакета<br /> * Соответствие определяется по шаблонам, заданным в конфигурации, и имени интерфейса сервиса в биллинге<br /> * Предполагается, что клиент авторизуется именно на том устройстве, на котором указан этот ProtocolHandler (не на дочерних)<br /> *<br /> * Параметры конфигурации:<br /> * radius.ipoe.nas_port_id.pattern.[i].pattern - регэксп-шаблон для имени интерфейса в биллинге<br /> * radius.ipoe.nas_port_id.pattern.[i].replacement - выражение для построения nas_port_id по шаблону<br /> *<br /> * Пример:<br /> * Gi0/0.1112 -&gt; 0/0/0/1112<br /> * Gi0/0.20150105 -&gt; 0/0/0/105.2015<br /> *<br /> */<br /> public class ISGIPoEProtocolHandler extends ISGProtocolHandler implements RadiusProtocolHandler {<br /> <br /> private static final Logger logger = Logger.getLogger( ISGIPoEProtocolHandler.class );<br /> <br /> /**<br /> * Кэш интерфейсов устройства<br /> */<br /> private volatile DeviceNasPortMap ifaceMap;<br /> <br /> @Override<br /> public void init(Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig) throws Exception {<br /> super.init(setup, moduleId, inetDevice, inetDeviceType, deviceConfig);<br /> this.ifaceMap = new DeviceNasPortMap(moduleId, inetDevice.getId(), deviceConfig.subIndexed(&quot;radius.ipoe.nas_port_id.pattern.&quot;));<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public void preprocessAccessRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccessRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета InetRadiusProcessor.INTERFACE_ID<br /> * @param request радиус-пакет<br /> */<br /> private void setBGIfaceId(RadiusPacket request) {<br /> String nas_port_id = request.getStringAttribute(-1, RadiusDictionary.NAS_Port_Id, null);<br /> Integer port=-1;<br /> if(nas_port_id!=null){<br /> port = this.ifaceMap.getIfacePort(nas_port_id);<br /> }<br /> if(null==port)<br /> {<br /> port=-1;//Насчёт port=0 и port=-1 - см http://forum.bgbilling.ru/viewtopic.php?f=44&amp;t=7694&amp;p=64541#p64541<br /> }<br /> request.setOption(InetRadiusProcessor.INTERFACE_ID, port);<br /> }<br /> <br /> /**<br /> * Кэш соответствий Nas-Port-Id -&gt; id интерфейса в биллинге для устройства<br /> * Обновляется при изменении порта или перезагрузке конфигурации<br /> */<br /> private class DeviceNasPortMap implements EventListener&lt;DeviceInterfaceModifiedEvent&gt; {<br /> /**<br /> * Соответствие cisco Nas-Port-Id -&gt; id интерфейса в биллинге<br /> */<br /> private volatile Map&lt;String, Integer&gt; nasPortIdToBGPortIdMap;<br /> private final int moduleId;<br /> private final int deviceId;<br /> /**<br /> * Список шаблонов-регулярных выражений, по которым будем получать Nas-Port-Id по названию интерфейса<br /> * Список паттернов не обновляется, т.к. берётся из конфига.<br /> * При перезагрузке конфига в любом случае ISGIPoEProtocolHandler будет переинициализирован целиком<br /> */<br /> private final SortedMap&lt;Integer, ParameterMap&gt; patternMap;<br /> <br /> public DeviceNasPortMap(int moduleId, int deviceId, SortedMap&lt;Integer, ParameterMap&gt; patternMap) throws BGException {<br /> this.moduleId = moduleId;<br /> this.deviceId = deviceId;<br /> this.patternMap = patternMap;<br /> EventProcessor.getInstance().addListener(this, DeviceInterfaceModifiedEvent.class);<br /> this.load();<br /> }<br /> <br /> private synchronized void load(){<br /> this.nasPortIdToBGPortIdMap = new HashMap&lt;String, Integer&gt;();<br /> //Перебираем порты устройств<br /> logger.info(&quot;(Re)loading DeviceNasPortMap for device &quot;+this.deviceId);<br /> ServerContext ctx = (ServerContext) ThreadContext.get();<br /> try {<br /> DeviceInterfaceService devicePortService = ctx.getService(DeviceInterfaceService.class, moduleId);<br /> List&lt;DeviceInterface&gt; deviceIfaceList = devicePortService.devicePortList(this.deviceId);<br /> String nasPortId;<br /> if(deviceIfaceList!=null){<br /> for(DeviceInterface iface : deviceIfaceList){<br /> nasPortId = nasPortIdByIfaceTitle(iface.getTitle());<br /> if(null!=nasPortId){<br /> nasPortIdToBGPortIdMap.put(nasPortId, iface.getPort());<br /> logger.debug(&quot;[device id=&quot; + this.deviceId + &quot;]: nas-port-id='&quot; + nasPortId + &quot;' -&gt; &quot; + iface.getPort());<br /> }<br /> }<br /> }<br /> } catch (BGException e) {<br /> logger.error(&quot;Error (re)loading DeviceNasPortMap&quot;, e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает Nas-Port-Id по имени интерфейса на основе регекспов из patternMap<br /> * @param ifaceTitle имя инерфейса (ex Gi0/0.123)<br /> * @return Nas-Port-Id (ex 0/0/0/123)<br /> */<br /> protected String nasPortIdByIfaceTitle(String ifaceTitle){<br /> if(null==ifaceTitle){<br /> return null;<br /> }<br /> Pattern p;<br /> Matcher m;<br /> String pattern;<br /> String replacement;<br /> String nasPortId;<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; patternMapEntry : patternMap.entrySet()){<br /> pattern = patternMapEntry.getValue().get(&quot;pattern&quot;, null);<br /> replacement = patternMapEntry.getValue().get(&quot;replacement&quot;, null);<br /> if(pattern!=null &amp;&amp; replacement!=null){<br /> p = Pattern.compile(pattern);<br /> m = p.matcher(ifaceTitle);<br /> if (m.find()) {<br /> //Получаем логин путём подстановки найденных capturing groups в $1, $2 и т.д. шаблона<br /> nasPortId = m.replaceFirst(replacement);<br /> return nasPortId;<br /> }<br /> }<br /> }<br /> return null;<br /> }<br /> <br /> /**<br /> * Получаем id порта в биллинге по nas_port_id из кэша<br /> */<br /> public Integer getIfacePort(String nas_port_id) {<br /> return this.nasPortIdToBGPortIdMap.get(nas_port_id);<br /> }<br /> <br /> /**<br /> * Обновляем кэш при изменении интерфейса<br /> * @throws BGException<br /> */<br /> @Override<br /> public void notify(DeviceInterfaceModifiedEvent event, EventListenerContext eventListenerContext) throws BGException {<br /> DeviceInterface deviceIface = event.getNewItem();<br /> if(deviceIface==null){<br /> deviceIface = event.getOldItem();<br /> }<br /> if(deviceIface!=null){<br /> if(deviceIface.getDeviceId()==this.deviceId){<br /> this.load();<br /> }<br /> }<br /> }<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> С 6.0:<br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.managed.ServerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.bitel.oss.systems.inventory.resource.common.DeviceInterfaceService;<br /> import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterface;<br /> import ru.bitel.oss.systems.inventory.resource.common.event.DeviceInterfaceModifiedEvent;<br /> <br /> import java.util.HashMap;<br /> import java.util.List;<br /> import java.util.Map;<br /> import java.util.SortedMap;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * ProtocolHandler для работы с cisco ip subscriber interface + cisco ISG<br /> * В предобработке устанавливается опция пакета InetRadiusProcessor.INTERFACE_ID, где указывается номер интерфейса в биллинге,<br /> * соответствующий атрибуту Nas-Port-Id из пакета<br /> * Соответствие определяется по шаблонам, заданным в конфигурации, и имени интерфейса сервиса в биллинге<br /> * Предполагается, что клиент авторизуется именно на том устройстве, на котором указан этот ProtocolHandler (не на дочерних)<br /> *<br /> * Параметры конфигурации:<br /> * radius.ipoe.nas_port_id.pattern.[i].pattern - регэксп-шаблон для имени интерфейса в биллинге<br /> * radius.ipoe.nas_port_id.pattern.[i].replacement - выражение для построения nas_port_id по шаблону<br /> *<br /> * Пример:<br /> * Gi0/0.1112 -&gt; 0/0/0/1112<br /> * Gi0/0.20150105 -&gt; 0/0/0/105.2015<br /> *<br /> */<br /> public class ISGIPoEProtocolHandler extends ISGProtocolHandler implements RadiusProtocolHandler, Destroyable {<br /> <br /> private static final Logger logger = Logger.getLogger( ISGIPoEProtocolHandler.class );<br /> <br /> /**<br /> * Кэш интерфейсов устройства<br /> */<br /> private volatile DeviceNasPortMap ifaceMap;<br /> <br /> @Override<br /> public void init(Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig) throws Exception {<br /> super.init(setup, moduleId, inetDevice, inetDeviceType, deviceConfig);<br /> this.ifaceMap = new DeviceNasPortMap(moduleId, inetDevice.getInvDeviceId(), deviceConfig.subIndexed(&quot;radius.ipoe.nas_port_id.pattern.&quot;));<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public void preprocessAccessRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccessRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета InetRadiusProcessor.INTERFACE_ID<br /> * @param request радиус-пакет<br /> */<br /> private void setBGIfaceId(RadiusPacket request) {<br /> String nas_port_id = request.getStringAttribute(-1, RadiusDictionary.NAS_Port_Id, null);<br /> Integer port=-1;<br /> if(nas_port_id!=null){<br /> port = this.ifaceMap.getIfacePort(nas_port_id);<br /> }<br /> if(null==port)<br /> {<br /> port=-1;//Насчёт port=0 и port=-1 - см http://forum.bgbilling.ru/viewtopic.php?f=44&amp;t=7694&amp;p=64541#p64541<br /> }<br /> request.setOption(InetRadiusProcessor.INTERFACE_ID, port);<br /> }<br /> <br /> @Override<br /> public void destroy() throws Exception {<br /> this.ifaceMap.destroy();<br /> }<br /> <br /> /**<br /> * Кэш соответствий Nas-Port-Id -&gt; id интерфейса в биллинге для устройства<br /> * Обновляется при изменении порта или перезагрузке конфигурации<br /> */<br /> private class DeviceNasPortMap implements EventListener&lt;DeviceInterfaceModifiedEvent&gt;, Destroyable {<br /> /**<br /> * Соответствие cisco Nas-Port-Id -&gt; id интерфейса в биллинге<br /> */<br /> private volatile Map&lt;String, Integer&gt; nasPortIdToBGPortIdMap;<br /> private final int moduleId;<br /> private final int deviceId;<br /> /**<br /> * Список шаблонов-регулярных выражений, по которым будем получать Nas-Port-Id по названию интерфейса<br /> * Список паттернов не обновляется, т.к. берётся из конфига.<br /> * При перезагрузке конфига в любом случае ISGIPoEProtocolHandler будет переинициализирован целиком<br /> */<br /> private final SortedMap&lt;Integer, ParameterMap&gt; patternMap;<br /> <br /> public DeviceNasPortMap(int moduleId, int deviceId, SortedMap&lt;Integer, ParameterMap&gt; patternMap) throws BGException {<br /> this.moduleId = moduleId;<br /> this.deviceId = deviceId;<br /> this.patternMap = patternMap;<br /> EventProcessor.getInstance().addListener(this, DeviceInterfaceModifiedEvent.class);<br /> this.load();<br /> }<br /> <br /> private synchronized void load(){<br /> this.nasPortIdToBGPortIdMap = new HashMap&lt;String, Integer&gt;();<br /> //Перебираем порты устройств<br /> logger.info(&quot;(Re)loading DeviceNasPortMap for device &quot;+this.deviceId);<br /> ServerContext ctx = (ServerContext) ThreadContext.get();<br /> try {<br /> DeviceInterfaceService devicePortService = ctx.getService(DeviceInterfaceService.class, moduleId);<br /> List&lt;DeviceInterface&gt; deviceIfaceList = devicePortService.devicePortList(this.deviceId);<br /> String nasPortId;<br /> if(deviceIfaceList!=null){<br /> for(DeviceInterface iface : deviceIfaceList){<br /> nasPortId = nasPortIdByIfaceTitle(iface.getTitle());<br /> if(null!=nasPortId){<br /> nasPortIdToBGPortIdMap.put(nasPortId, iface.getPort());<br /> logger.debug(&quot;[device id=&quot; + this.deviceId + &quot;]: nas-port-id='&quot; + nasPortId + &quot;' -&gt; &quot; + iface.getPort());<br /> }<br /> }<br /> }<br /> } catch (BGException e) {<br /> logger.error(&quot;Error (re)loading DeviceNasPortMap&quot;, e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает Nas-Port-Id по имени интерфейса на основе регекспов из patternMap<br /> * @param ifaceTitle имя инерфейса (ex Gi0/0.123)<br /> * @return Nas-Port-Id (ex 0/0/0/123)<br /> */<br /> protected String nasPortIdByIfaceTitle(String ifaceTitle){<br /> if(null==ifaceTitle){<br /> return null;<br /> }<br /> Pattern p;<br /> Matcher m;<br /> String pattern;<br /> String replacement;<br /> String nasPortId;<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; patternMapEntry : patternMap.entrySet()){<br /> pattern = patternMapEntry.getValue().get(&quot;pattern&quot;, null);<br /> replacement = patternMapEntry.getValue().get(&quot;replacement&quot;, null);<br /> if(pattern!=null &amp;&amp; replacement!=null){<br /> p = Pattern.compile(pattern);<br /> m = p.matcher(ifaceTitle);<br /> if (m.find()) {<br /> //Получаем логин путём подстановки найденных capturing groups в $1, $2 и т.д. шаблона<br /> nasPortId = m.replaceFirst(replacement);<br /> return nasPortId;<br /> }<br /> }<br /> }<br /> return null;<br /> }<br /> <br /> /**<br /> * Получаем id порта в биллинге по nas_port_id из кэша<br /> */<br /> public Integer getIfacePort(String nas_port_id) {<br /> return this.nasPortIdToBGPortIdMap.get(nas_port_id);<br /> }<br /> <br /> /**<br /> * Обновляем кэш при изменении интерфейса<br /> * @throws BGException<br /> */<br /> @Override<br /> public void notify(DeviceInterfaceModifiedEvent event, EventListenerContext eventListenerContext) throws BGException {<br /> DeviceInterface deviceIface = event.getNewItem();<br /> if(deviceIface==null){<br /> deviceIface = event.getOldItem();<br /> }<br /> if(deviceIface!=null){<br /> if(deviceIface.getDeviceId()==this.deviceId){<br /> this.load();<br /> }<br /> }<br /> }<br /> <br /> @Override<br /> public void destroy() throws Exception {<br /> if(logger.isDebugEnabled()) {<br /> logger.debug(&quot;destroy for deviceId = &quot;+this.deviceId);<br /> }<br /> EventProcessor.getInstance().removeListener(this);<br /> }<br /> <br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> <br /> <br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.dhcp.DhcpProtocolHandler;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> <br /> import java.util.Collections;<br /> import java.util.LinkedHashMap;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * Базовый класс для Cisco ISG<br /> * Копипаста бителовского, без лишней потехи с option 82<br /> */<br /> public class ISGProtocolHandler<br /> extends AbstractRadiusProtocolHandler<br /> implements RadiusProtocolHandler, DhcpProtocolHandler<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGProtocolHandler.class );<br /> <br /> /**<br /> * Код атрибута - id родительского аккаунтинга<br /> */<br /> protected int parentAcctSessionIdType;<br /> <br /> /**<br /> * Префикс id родительского аккаунтинга<br /> */<br /> protected String parentAcctSessionIdPrefix;<br /> <br /> /**<br /> * Код атрибута - имя сервиса (для cisco-avpair)<br /> */<br /> protected int serviceNameType;<br /> <br /> /**<br /> * Префикс имени сервиса (для cisco-avpair)<br /> */<br /> protected String serviceNamePrefix;<br /> <br /> /**<br /> * Имя сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> public ISGProtocolHandler()<br /> {<br /> super( 9 ); // Cisco<br /> }<br /> <br /> @Override<br /> public void init( Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, inetDevice, inetDeviceType, deviceConfig );<br /> <br /> parentAcctSessionIdType = deviceConfig.getInt( &quot;radius.parentAcctSessionId.type&quot;, 1 ); // cisco-avpair<br /> parentAcctSessionIdPrefix = deviceConfig.get( &quot;radius.parentAcctSessionId.prefix&quot;, &quot;parent-session-id=&quot; );<br /> serviceNameType = deviceConfig.getInt( &quot;radius.serviceName.type&quot;, 251 ); // cisco-SSG-Service-Info<br /> serviceNamePrefix = deviceConfig.get( &quot;radius.serviceName.prefix&quot;, &quot;&quot; );<br /> <br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )<br /> throws Exception<br /> {<br /> int acctStatusType = request.getIntAttribute( -1, RadiusDictionary.Acct_Status_Type, -1 );<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> <br /> // извлекаем parentAcctSessionId<br /> String parentAcctSessionId;<br /> // если parentAcctSessionId находится в cisco-avpair - то нужно искать по префиксу<br /> if( parentAcctSessionIdType == 1 )<br /> {<br /> parentAcctSessionId = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; attributes = request.getAttributes( radiusVendor, parentAcctSessionIdType );<br /> if( attributes != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; attr : attributes )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> String value = ((RadiusAttribute&lt;String&gt;)attr).getValue();<br /> if( value.startsWith( parentAcctSessionIdPrefix ) )<br /> {<br /> parentAcctSessionId = value.substring( parentAcctSessionIdPrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> parentAcctSessionId = request.getStringAttribute( radiusVendor, parentAcctSessionIdType, null );<br /> }<br /> <br /> // если это аккаунтинг сервисной сессии<br /> if( parentAcctSessionId != null )<br /> {<br /> // извлекаем serviceName<br /> String serviceName;<br /> // если serviceName находится в cisco-avpair - то нужно искать по префиксу<br /> if( serviceNameType == 1 )<br /> {<br /> serviceName = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> final List&lt;RadiusAttribute&lt;?&gt;&gt; ras = request.getAttributes( radiusVendor, serviceNameType );<br /> if( ras != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; ra : ras )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> final String value = ((RadiusAttribute&lt;String&gt;)ra).getValue();<br /> if( value.startsWith( serviceNamePrefix ) )<br /> {<br /> serviceName = value.substring( serviceNamePrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> serviceName = request.getStringAttribute( radiusVendor, serviceNameType, null );<br /> }<br /> <br /> if( serviceName == null || !serviceName.startsWith( &quot;N&quot; ) )<br /> {<br /> logger.error( &quot;Parent acctSessionId found, but ServiceName is not&quot; );<br /> }<br /> else<br /> {<br /> serviceName = serviceName.substring( 1 );<br /> }<br /> <br /> // устанавливаем id родительской сессии<br /> request.setOption( InetRadiusProcessor.PARENT_ACCT_SESSION_ID, parentAcctSessionId );<br /> // устанавливаем имя сервиса текущего аккаунтинга<br /> request.setOption( InetRadiusProcessor.SERVICE_NAME, serviceName );<br /> <br /> // если указан сервис, при котором доступ ограничен - проверяем, не его ли это аккаунтинг,<br /> // и, если это так, переключаем состояние соединения<br /> if( disableServiceNames != null &amp;&amp; disableServiceNames.contains( serviceName ) )<br /> {<br /> // start или update<br /> if( acctStatusType == 1 || acctStatusType == 3 )<br /> {<br /> logger.debug( &quot;State is disable (from start disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_DISABLE );<br /> }<br /> else<br /> {<br /> logger.debug( &quot;State is enable (from stop disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_ENABLE );<br /> }<br /> }<br /> }<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> === Устройства ===<br /> Дерево устройств:<br /> <br /> [[Файл:devices.png]]<br /> <br /> Интерфейсы:<br /> <br /> [[Файл:device-ifaces.png]]<br /> <br /> === IP-ресурсы ===<br /> Не используются.<br /> === VLAN-ресурсы ===<br /> Не используются.<br /> <br /> == Настройка BGInetAccess и BGInetAccounting ==<br /> <br /> === BGInetAccess-VPN ===<br /> <br /> '''access.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3851<br /> MEMORY=-Xmx512m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-access.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;4&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;3&quot;/&gt;<br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3812&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> === BGInetAccess-ISG ===<br /> '''TODO''': описать настройку справочника сервисов ISG.<br /> <br /> У нас для этих целей сейчас используется модуль Dialup - так исторически сложилось.<br /> Но по-хорошему, нужно настроить отдельный BGInetAccess, повешать его на отдельную ProcessGroup и авторизовать сервисы из него со специального служебного договора.<br /> Пример есть в статье [[ISG,_схема_со_стартом_сессии_и_ее_авторизацией_по_IP,_выдача_адресов_на_основе_option82_(Конфигурация_BGBilling%27а)]] (см. 'ASR ISG Service')<br /> <br /> === BGInetAccounting-VPN ===<br /> <br /> '''accounting.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccounting-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-accounting.xml&quot;<br /> NAME=inet-accounting<br /> NAME_SHORT=accounting<br /> ADMIN_PORT=3852<br /> MEMORY=-Xmx1024m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &quot;$@&quot;<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-accounting.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;accounting&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccounting-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;5&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения radius-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Параметры сохранения flow-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.flow.dir&quot; value=&quot;data/flow&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл и поток слушателя --&gt;<br /> &lt;param name=&quot;datalog.flow.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.flow.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Создание Accounting --&gt;<br /> &lt;bean name=&quot;accounting&quot; class=&quot;ru.bitel.bgbilling.modules.inet.accounting.Accounting&quot;/&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3813&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;1 * 1024 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;30&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;500&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.accounting --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.accounting&lt;/param&gt;<br /> &lt;!-- Передача setup --&gt;<br /> &lt;param name=&quot;setup&quot;&gt;setup&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> == Тарифы ==<br /> <br /> Безлимит 1Мбит:<br /> <br /> [[Файл:vpn-unlim.png]]<br /> <br /> = Конфигурация на cisco =<br /> <br /> Пример конфига интерфейса клиента:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> interface GigabitEthernet0/0.127<br /> description ---- TEST VPN ----<br /> encapsulation dot1Q 127<br /> ip vrf forwarding vpn-test<br /> ip address IP.IP.IP.IP MASK.MASK.MASK.MASK<br /> no ip redirects<br /> no ip proxy-arp<br /> ip verify unicast source reachable-via rx<br /> no cdp enable<br /> service-policy type control IP-ISG-VRFSUB<br /> ip subscriber interface<br /> end<br /> &lt;/source&gt;<br /> <br /> ...<br /> <br /> = Дополнительно =<br /> *[[Мониторинг_Inet-Radius_через_JMX]]<br /> * Последние версии кода: [https://github.com/Cromeshnic/BGTools/ на github]<br /> *[[Справочник_Cisco-ISG_сервисов]]<br /> <br /> = TODO =<br /> *Описать настройку [[http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 справочника сервисов ISG]] (Done)<br /> *Выложить дин. код на github<br /> *Описать подключение netflow для детализации<br /> *Скрипт обновления/проставления ifindex на интерфейсах в BG<br /> *Автоконфигурирование интерфейса по telnet/ssh при создании/удалении<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 05:11, 25 июля 2013 (UTC)</div> Mon, 25 Sep 2017 06:48:38 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG Участник:Cromeshnic http://wiki.bitel.ru/index.php/%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:Cromeshnic <p>Cromeshnic:&#32;</p> <hr /> <div>cromeshnic [at] gmail.com<br /> <br /> = Статьи =<br /> * [[Персональные цены для договоров]]<br /> * [[Quota Manager]]<br /> * [[Справочник Cisco-ISG сервисов]]<br /> * [[Vlan per user + Cisco IP subscriber interface + ISG]]<br /> * [[Заказ IP-детализации из личного кабинета]]<br /> * [[Обработчик управления устройством с синхронизацией интерфейсов и их индексов]]<br /> * [[Мониторинг Inet-Radius через JMX]]<br /> * [[Мониторинг java-процессов по snmp]]<br /> * [[Сравнение прав пользователей]]<br /> * [[Метки услуг]]<br /> * [[Javaws]]<br /> * [[Импорт старой схемы рассылок баланса в Dispatch]]<br /> * [[Глобальное событие запуска сервера]]<br /> * [[WebAction CustomSuspend]] <br /> <br /> = Скинуться на кофе: =<br /> <br /> Рокет - https://rocketbank.ru/semen-koshechkin-aged-field<br /> <br /> Я.Деньги - 41001959637999</div> Mon, 29 May 2017 06:12:41 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%83%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA%D0%B0:Cromeshnic Участник:Cromeshnic http://wiki.bitel.ru/index.php/%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:Cromeshnic <p>Cromeshnic:&#32;</p> <hr /> <div>cromeshnic [at] gmail.com<br /> <br /> На кофе:<br /> <br /> Рокет - https://rocketbank.ru/semen-koshechkin-aged-field<br /> <br /> Я.Деньги - 41001959637999</div> Mon, 29 May 2017 05:54:06 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%83%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA%D0%B0:Cromeshnic Участник:Cromeshnic http://wiki.bitel.ru/index.php/%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:Cromeshnic <p>Cromeshnic:&#32;</p> <hr /> <div>cromeshnic [at] gmail.com<br /> <br /> На кофе:<br /> Рокет - https://rocketbank.ru/semen-koshechkin-aged-field<br /> Я.Деньги - 41001959637999</div> Mon, 29 May 2017 05:53:54 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%83%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA%D0%B0:Cromeshnic Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> <br /> Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.<br /> <br /> ==== Веб-сервис переопределения цен ====<br /> <br /> Вспомогательные классы:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import ru.bitel.common.model.Id;<br /> <br /> import java.io.Serializable;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Тарифный план договора для использования в веб-сервисе.<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на веб-сервис, будем делать так у себя<br /> *<br /> */<br /> public class ContractTariff extends Id implements Serializable {<br /> private int contractId;<br /> private int tariffPlanId;<br /> private Date date1;<br /> private Date date2;<br /> private String comment = &quot;&quot;;<br /> private String tariffName;<br /> private int pos;<br /> private int entityMid;<br /> private int entityId;<br /> private int replacedFromContractTariffId;<br /> <br /> public ContractTariff() {<br /> this.id = -1;<br /> this.contractId = -1;<br /> this.tariffPlanId = -1;<br /> this.date1 = null;<br /> this.date2 = null;<br /> this.tariffName = null;<br /> this.pos = 0;<br /> this.entityId = 0;<br /> this.entityMid = 0;<br /> this.replacedFromContractTariffId = -1;<br /> }<br /> <br /> public int getContractId() {<br /> return contractId;<br /> }<br /> <br /> public void setContractId(int contractId) {<br /> this.contractId = contractId;<br /> }<br /> <br /> public int getTariffPlanId() {<br /> return tariffPlanId;<br /> }<br /> <br /> public String getTariffName() {<br /> return tariffName;<br /> }<br /> <br /> public void setTariffName(String tariffName) {<br /> this.tariffName = tariffName;<br /> }<br /> <br /> public void setTariffPlanId(int tariffPlanId) {<br /> this.tariffPlanId = tariffPlanId;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> <br /> public String getComment() {<br /> return comment;<br /> }<br /> <br /> public void setComment(String comment) {<br /> this.comment = comment;<br /> }<br /> <br /> public int getPos() {<br /> return pos;<br /> }<br /> <br /> public void setPos(int pos) {<br /> this.pos = pos;<br /> }<br /> <br /> public int getEntityMid() {<br /> return entityMid;<br /> }<br /> <br /> public void setEntityMid(int entityMid) {<br /> this.entityMid = entityMid;<br /> }<br /> <br /> public int getEntityId() {<br /> return entityId;<br /> }<br /> <br /> public void setEntityId(int entityId) {<br /> this.entityId = entityId;<br /> }<br /> <br /> public int getReplacedFromContractTariffId() {<br /> return replacedFromContractTariffId;<br /> }<br /> <br /> public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {<br /> this.replacedFromContractTariffId = replacedFromContractTariffId;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на веб-сервис, будем пользоваться собственным сервисом<br /> *<br /> */<br /> @WebService<br /> @XmlSeeAlso({ContractTariff.class})<br /> public abstract interface ContractTariffService {<br /> @WebMethod<br /> public abstract List&lt;ContractTariff&gt; contractTariffList(@WebParam(name=&quot;contractId&quot;) int contracId) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.util.List;<br /> <br /> /**<br /> * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService&quot;)<br /> public class ContractTariffServiceImpl extends AbstractService<br /> implements ContractTariffService {<br /> <br /> @Override<br /> public List&lt;ContractTariff&gt; contractTariffList(@WebParam(name = &quot;contractId&quot;) int contracId) throws BGException {<br /> return (new ContractTariffDao(getConnection()).list(contracId));<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Веб-сервис работы с переопределёнными ценами:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Интерфейс веб-сервиса для работы с персональными ценами<br /> */<br /> @WebService<br /> @XmlSeeAlso({CustomTariffCost.class})<br /> public abstract interface CustomTariffCostService {<br /> <br /> @WebMethod<br /> public abstract List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id)<br /> throws BGException;<br /> <br /> @WebMethod<br /> public abstract void updateCustomTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs)<br /> throws BGException;<br /> <br /> /**<br /> * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> */<br /> @WebMethod<br /> public abstract Set&lt;Integer&gt; availableServiceList(@WebParam(name=&quot;tariffPlanId&quot;) int tpid) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;<br /> import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;<br /> import ru.bitel.bgbilling.server.util.ClosedDateChecker;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.List;<br /> import java.util.Set;<br /> import java.util.TreeSet;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * Реализация веб-сервиса для работы с персональными ценам<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService&quot;)<br /> public class CustomTariffCostServiceImpl extends AbstractService<br /> implements CustomTariffCostService {<br /> <br /> @Override<br /> public List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name = &quot;contractTariffId&quot;) int contract_tariff_id) throws BGException {<br /> return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);<br /> }<br /> <br /> @Override<br /> public void updateCustomTariffCostList(@WebParam(name = &quot;contractTariffId&quot;)int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs) throws BGException{<br /> <br /> Calendar closedDate;<br /> ContractTariffManager ctm = new ContractTariffManager(getConnection());<br /> ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);<br /> if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking(&quot;updateCustomTariffCostList&quot;, 0, this.userId)) != null)<br /> {<br /> ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());<br /> }<br /> CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());<br /> try {<br /> ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);<br /> } catch (SQLException e) {<br /> throw new BGException(e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> * На данный момент реализовано через метки - ищем все метки тарифного плана вида &quot;id - Название&quot;,<br /> * где id - это id услуги. Например &quot;5 - Интернет&quot;.<br /> */<br /> @Override<br /> public Set&lt;Integer&gt; availableServiceList(@WebParam(name = &quot;tariffPlanId&quot;) int tpid)<br /> throws BGException<br /> {<br /> Set&lt;Integer&gt; result = new TreeSet&lt;Integer&gt;();<br /> TariffLabelManager tlm = new TariffLabelManager(getConnection());<br /> //Получаем список id меток тарифа<br /> Set&lt;Integer&gt; tariffLabelIds = tlm.getTariffLabelIds(tpid);<br /> if(tariffLabelIds.size()&gt;0){<br /> //Берём список всех меток с названиями<br /> List&lt;TariffLabelItem&gt; tariffLabelItemList = tlm.getTariffLabelItemList();<br /> //Ищем среди меток тарифа метки с названиями вида &quot;id - Название&quot;<br /> /*for(Integer label_id : tariffLabelIds){<br /> TariffLabelItem label = tariffLabelItemList.get()<br /> }*/<br /> Pattern pattern = Pattern.compile(&quot;^(\\d+) - .+$&quot;);<br /> <br /> Matcher matcher;<br /> //Перебираем все метки из справочника<br /> for(TariffLabelItem label : tariffLabelItemList){<br /> //Если метка есть на нашем тарифе ...<br /> if(tariffLabelIds.contains(label.getId())){<br /> //...то смотрим её название<br /> matcher = pattern.matcher(label.getTitle());<br /> if(matcher.find()){//определился sid услуги - отлично, добавляем в результат<br /> try{<br /> result.add(Integer.valueOf(matcher.group(1)));<br /> }catch (Exception e){}<br /> }<br /> }<br /> }<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> ==== Клиетский интерфейс ====<br /> <br /> Для начала немного костылей.<br /> <br /> Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:<br /> <br /> [[Файл:custom_cost_labels.jpg]]<br /> <br /> Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида &quot;xxx - Наименование&quot;, где xxx - sid услуги, дополненный нулями до 3 символов, и проставить эту метку для всех необходимых тарифов.<br /> <br /> Переходим собственно к интерфейсу.<br /> <br /> Менять клиентский интерфейс биллинга стандартным образом нельзя - только через переопределение jar-файлов клиента.<br /> <br /> Я долго думал, куда бы запихать управление персональными ценами малой кровью.<br /> По-идее, будь я разработчиком Битела, я бы сделал это в ядре (т.к. в общем случае можно реализовать переопределение не только абонплат). Соответственно, редактирование персональных цен должно быть где-то в разделе &quot;Тарифные планы&quot; договора - например, отдельной вкладкой после &quot;Персональных тарифов&quot; и &quot;Тарифных опций&quot;.<br /> Но малой кровью модифицировать эту клиентскую панель не получилось, поэтому было решено сделать отдельную вкладку &quot;Переопределение цен&quot; на странице модуля Npay - см http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost.jpg<br /> <br /> <br /> Привожу клиентский код для 5.2<br /> Что с ним делать - решайте сами на свой страх и риск :)<br /> <br /> Добавляем нашу панель - прописываем в клиентский файл bitel/billing/module/services/npay/setup_user.properties:<br /> <br /> &lt;source lang=ini&gt;<br /> module.tab.3.class=bitel.billing.module.services.npay.CustomCostModulePanel<br /> module.tab.3.title=\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0446\u0435\u043D<br /> &lt;/source&gt;<br /> <br /> Панель:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.module.common.BGControlPanelContractSelect;<br /> import bitel.billing.module.common.BGTitleBorder;<br /> import bitel.billing.module.common.DialogToolBar;<br /> import bitel.billing.module.common.FloatTextField;<br /> import ru.bitel.bgbilling.client.common.BGEditor;<br /> import ru.bitel.bgbilling.client.common.BGUPanel;<br /> import ru.bitel.bgbilling.client.common.BGUTable;<br /> import ru.bitel.bgbilling.client.common.ClientContext;<br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.Service;<br /> import ru.bitel.bgbilling.kernel.module.common.service.ServiceService;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.client.AbstractBGUPanel;<br /> import ru.bitel.common.client.BGSwingUtilites;<br /> import ru.bitel.common.client.BGUComboBox;<br /> import ru.bitel.common.client.table.BGTableModel;<br /> import ru.bitel.common.model.KeyValue;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.swing.*;<br /> import java.awt.*;<br /> import java.awt.event.ActionEvent;<br /> import java.awt.event.ActionListener;<br /> import java.math.BigDecimal;<br /> import java.util.*;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Панелька на клиенте для редактирования переопределений стоимости тарифных планов договоров.<br /> */<br /> public class CustomCostModulePanel extends BGUPanel<br /> {<br /> private BGControlPanelContractSelect contractSelect;<br /> private ContractTariffService wsContractTariff = getContext().getPort(ContractTariffService.class, 0);<br /> <br /> private BGTableModel&lt;ContractTariff&gt; model = new BGTableModel&lt;ContractTariff&gt;(&quot;contractTariff&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, Integer.class, 0, 0, 0, &quot;id&quot;, true, false);<br /> addColumn(&quot;TPID&quot;, Integer.class, 0, 0, 0, &quot;tpid&quot;, true, false);<br /> addColumn(&quot;Название&quot;, &quot;tariffName&quot;, true);<br /> addColumn(&quot;Период&quot;, &quot;period&quot;, true);<br /> addColumn(&quot;Комментарий&quot;, &quot;comment&quot;, false);<br /> addColumn(&quot;Поз.&quot;, &quot;pos&quot;, true);<br /> }<br /> <br /> @Override<br /> public Object getValue(ContractTariff val, int column)<br /> {<br /> Object result = &quot;&quot;;<br /> switch (column)<br /> {<br /> case 3:<br /> result = TimeUtils.formatPeriod(val.getDate1(), val.getDate2());<br /> break;<br /> default:<br /> result = super.getValue(val, column);<br /> }<br /> return result;<br /> }<br /> };<br /> <br /> public CustomCostModulePanel()<br /> {<br /> super(new BorderLayout());<br /> //setLayout(new GridBagLayout());<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;refresh&quot;, &quot;Обновить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostModulePanel.this.setData();<br /> }<br /> };<br /> }<br /> <br /> public void setData() throws BGException {<br /> String cids = CustomCostModulePanel.this.contractSelect.getContracts();<br /> int cid = -1;<br /> try{<br /> cid = Utils.toIntegerList(cids).get(0);<br /> }catch(Exception ex){<br /> this.processException(ex);<br /> }<br /> <br /> CustomCostModulePanel.this.model.setData(CustomCostModulePanel.this.wsContractTariff.contractTariffList(cid));<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> setLayout(new GridBagLayout());<br /> BGUTable table = new BGUTable(this.model);<br /> <br /> JButton okButton = new JButton(&quot;OK&quot;);<br /> okButton.setMargin(new Insets(2, 2, 2, 2));<br /> okButton.addActionListener(new ActionListener() {<br /> public void actionPerformed(ActionEvent e) {<br /> try {<br /> CustomCostModulePanel.this.setData();<br /> } catch (BGException e1) {<br /> processException(e1);<br /> }<br /> }<br /> });<br /> this.contractSelect = new BGControlPanelContractSelect(true, true);<br /> this.contractSelect.add(okButton, new GridBagConstraints(2, 0, 1, 1, 0.0D, 0.0D, 10, 0,<br /> new Insets(0, 0, 5, 5), 0, 0));<br /> <br /> add(BGSwingUtilites.wrapBorder(this.contractSelect, &quot;Выберите договор&quot;), new GridBagConstraints(0, 0, 1, 1, 1.0D, 0.0D, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.setVisible(false);<br /> editor.addForm(new CustomCostTariffPanel(getContext()));<br /> add(BGSwingUtilites.wrapBorder(new JScrollPane(table), &quot;Тарифные планы договора&quot;),<br /> new GridBagConstraints(0, 1, 1, 1, 1.0D, 0.6D, 10, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 1.0D, GridBagConstraints.SOUTH, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> BGSwingUtilites.handleEdit(table, editor);<br /> }<br /> <br /> /**<br /> * Панель редактирования собственно списка переопределений услуг конкретного тарифа договора<br /> */<br /> public class CustomCostTariffPanel extends BGUPanel {<br /> private BGUTable table;<br /> CustomTariffCostService wsCustomCost = this.getContext().getPort(CustomTariffCostService.class, 0);<br /> private Map&lt;Integer, Service&gt; servicesMap;//Справочник услуг всех модулей : sid -&gt; Service<br /> private ContractTariff current;<br /> <br /> private BGTableModel&lt;CustomCostRow&gt; model = new BGTableModel&lt;CustomCostRow&gt;(&quot;customCosts&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, 0, 0, 0, null, true);<br /> addColumn(&quot;Услуга&quot;, -1, 120, -1, null, false);<br /> addColumn(&quot;Цена&quot;, BigDecimal.class, -1, 180, -1, null, false);<br /> }<br /> <br /> public Object getValue(CustomCostRow val, int column)<br /> {<br /> switch (column)<br /> {<br /> case 0:<br /> return val.sid;<br /> case 1:<br /> return val.title;<br /> case 2:<br /> return val.value;<br /> default:<br /> return super.getValue(val, column);<br /> }<br /> }<br /> };<br /> <br /> protected void initActions(){<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if ((CustomCostTariffPanel.this.current = CustomCostModulePanel.this.model.getSelectedRow()) != null)<br /> {<br /> CustomCostTariffPanel.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;Сохранить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if(CustomCostTariffPanel.this.current!=null){<br /> //CustomCostTariffPanel.this.model.getRows();<br /> List&lt;CustomTariffCost&gt; costs = new ArrayList&lt;CustomTariffCost&gt;();<br /> if(CustomCostTariffPanel.this.model.getRows()!=null){<br /> CustomTariffCost cost;<br /> for(CustomCostRow row : CustomCostTariffPanel.this.model.getRows()){<br /> cost = new CustomTariffCost(CustomCostTariffPanel.this.current.getId(),row.sid,row.value);<br /> costs.add(cost);<br /> }<br /> }<br /> CustomCostTariffPanel.this.wsCustomCost.updateCustomTariffCostList(<br /> CustomCostTariffPanel.this.current.getId(),<br /> costs<br /> );<br /> }<br /> CustomCostTariffPanel.this.performActionClose();<br /> }<br /> };<br /> }<br /> <br /> @Override<br /> public void performActionOpen()<br /> {<br /> //расставляем данные в форме редактирования, затем отображаем её<br /> setData(this.current.getId(), this.current.getTariffPlanId());<br /> super.performActionOpen();<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> //To change body of implemented methods use File | Settings | File Templates.<br /> setLayout(new GridBagLayout());<br /> this.table = new BGUTable(this.model);<br /> DialogToolBar toolBar = new DialogToolBar();<br /> <br /> ParamForm paramForm = new ParamForm(getContext());<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.addForm(paramForm);<br /> editor.setVisible(false);<br /> <br /> BGSwingUtilites.buildToolBar(toolBar, paramForm);<br /> toolBar.compact();<br /> <br /> BGSwingUtilites.handleEdit(this.table, editor);<br /> <br /> add(toolBar, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 0), 0, 0));<br /> add(new JScrollPane(this.table), new GridBagConstraints(0, 1, 1, 1, 1.0D, 1.0D, 10, 1, new Insets(0, 0,<br /> 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(5, 5, 5, 5), 0, 0));<br /> }<br /> <br /> //Сравниваем строчки цен для сортировки в таблице<br /> private final Comparator&lt;CustomCostRow&gt; COMPARATOR = new Comparator&lt;CustomCostRow&gt;()<br /> {<br /> public int compare(CustomCostRow o1, CustomCostRow o2)<br /> {<br /> int result = Integer.valueOf(o1.sid).compareTo(o2.sid);<br /> if (result != 0)<br /> {<br /> return result;<br /> }<br /> <br /> return o1.title.compareTo(o2.title);<br /> }<br /> };<br /> <br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> private BGUComboBox&lt;KeyValue&lt;Integer, String&gt;&gt; keyCombo = new BGUComboBox(<br /> KeyValue.class);<br /> <br /> public CustomCostTariffPanel(ClientContext context)<br /> {<br /> super(context);<br /> //Кэшируем список всех услуг биллинга. Если возникнет ошибка при его получении - ругаемся<br /> try {<br /> java.util.List&lt;Service&gt; serviceList = CustomCostTariffPanel.this.getContext().getPort(ServiceService.class, 0).serviceList(0);<br /> this.servicesMap = new HashMap&lt;Integer, Service&gt;(serviceList.size());<br /> for(Service s : serviceList){<br /> this.servicesMap.put(s.getId(),s);<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> }<br /> }<br /> <br /> public void setData(int id, int tpid){<br /> java.util.List&lt;CustomTariffCost&gt; costList;<br /> try {<br /> costList = wsCustomCost.customTariffCostList(id);<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> java.util.List&lt;CustomCostRow&gt; data = new ArrayList&lt;CustomCostRow&gt;(costList.size());<br /> String title;<br /> Service s;<br /> for(CustomTariffCost cost : costList){<br /> s = this.servicesMap.get(cost.getSid());<br /> if(s!=null){<br /> title = s.getTitle();<br /> }else{<br /> title = String.valueOf(cost.getSid());<br /> }<br /> data.add(new CustomCostRow(cost.getSid(), title, cost.getValue()));<br /> }<br /> Collections.sort(data, COMPARATOR);<br /> this.model.setData(data);<br /> <br /> //Получаем для комбобокса список услуг, для которых разрешено переопределение на этом тарифе<br /> java.util.List&lt;KeyValue&lt;Integer, String&gt;&gt; typeList = new ArrayList&lt;KeyValue&lt;Integer, String&gt;&gt;(CustomCostTariffPanel.this.servicesMap.size());<br /> try{<br /> for(Integer sid : this.wsCustomCost.availableServiceList(tpid)){<br /> s = this.servicesMap.get(sid);<br /> if(null!=s){<br /> title = s.getTitle();<br /> }else{<br /> title = &quot;&lt;&quot;+sid+&quot;&gt;&quot;;<br /> }<br /> <br /> typeList.add(new KeyValue&lt;Integer, String&gt;(sid, title));<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> CustomCostTariffPanel.this.keyCombo.setData(typeList);<br /> }<br /> <br /> class CustomCostRow<br /> {<br /> public int sid;<br /> public String title;<br /> public BigDecimal value;<br /> <br /> public CustomCostRow(int sid, String title, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.title = title;<br /> this.value = value;<br /> }<br /> <br /> /*public CustomCostRow(int sid, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.value = value;<br /> }*/<br /> }<br /> <br /> class ParamForm extends BGUPanel<br /> {<br /> private FloatTextField value = new FloatTextField();<br /> private CustomCostRow current;<br /> private boolean edit;<br /> <br /> public ParamForm(ClientContext context)<br /> {<br /> super(context);<br /> }<br /> <br /> protected void jbInit()<br /> {<br /> setBorder(BorderFactory.createCompoundBorder(new BGTitleBorder(&quot; Редактор &quot;), BorderFactory.createEmptyBorder(0, 3, 3, 3)));<br /> <br /> add(CustomCostTariffPanel.this.keyCombo, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 5), 0, 0));<br /> <br /> add(this.value, new GridBagConstraints(1, 0, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(0, 0, 0, 0),<br /> 0, 0));<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;new&quot;, &quot;Добавить&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = false;<br /> CustomCostTariffPanel.ParamForm.this.current = new CustomCostRow(0,&quot;&quot;,BigDecimal.ZERO);<br /> <br /> CustomCostTariffPanel.ParamForm.this.value.setText(&quot;&quot;);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(true);<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = true;<br /> <br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.keyCombo.setSelectedItem(ParamForm.this.current.sid);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(false);<br /> <br /> ParamForm.this.value.setText(String.valueOf(ParamForm.this.current.value));<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;delete&quot;, &quot;Удалить&quot;, ClientUtils.getIcon(&quot;item_delete&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> <br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.model.deleteSelectedRow();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;OK&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> KeyValue&lt;Integer, String&gt; key = CustomCostTariffPanel.this.keyCombo.getSelectedItem();<br /> if (key == null)<br /> {<br /> return;<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.current.sid = key.getKey();<br /> CustomCostTariffPanel.ParamForm.this.current.title = key.getValue();<br /> CustomCostTariffPanel.ParamForm.this.current.value = Utils.parseBigDecimal(CustomCostTariffPanel.ParamForm.this.value.getText(), BigDecimal.ZERO);<br /> <br /> if (!CustomCostTariffPanel.ParamForm.this.edit)<br /> {<br /> CustomCostTariffPanel.this.model.addRow(CustomCostTariffPanel.ParamForm.this.current);<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionClose();<br /> }<br /> };<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> Клиентские узлы тарифа:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел DayModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;за день&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;за месяц&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;за день&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;за месяц&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел MonthModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> <br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;безусловно&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;пропорц. периоду&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;2&quot;, &quot;пропорц. объему&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;3&quot;, &quot;как выгоднее&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;безусловно&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;пропорц. периоду&quot;);<br /> }<br /> else if (this.type.equals(&quot;2&quot;))<br /> {<br /> title.append(&quot;пропорц. объему&quot;);<br /> }<br /> else if (this.type.equals(&quot;3&quot;))<br /> {<br /> title.append(&quot;как выгоднее&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел YearModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Стоимость: &quot;));<br /> edit.add(this.costTf);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(&quot;Стоимость: &quot;);<br /> title.append(this.cost);<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.custom = Utils.parseBoolean((String) data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> this.costTf.setText(this.cost);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> === Отображение на договоре ===<br /> <br /> На договоре персональные цены на услуги будем писать другим шрифтом в скобках через запятую после наименования тарифа в дереве.<br /> <br /> Для этого переопределим через dynaction стандартный action вывода карточки договора:<br /> <br /> &lt;source lang=ini&gt;<br /> dynaction:contract.hierarchy.ActionContractInfo=ru.dsi.bgbilling.kernel.scripts.action.ActionContractInfo<br /> &lt;/source&gt;<br /> <br /> Переопределённый action делает много всяких штук, кроме подсветки персональных цен. Привожу целиком, лишнее можно убрать по необходимости:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.scripts.action.contract;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.ApplicationModule;<br /> import bitel.billing.server.call.bean.Login;<br /> import bitel.billing.server.contract.bean.ContractModuleManager;<br /> import bitel.billing.server.contract.bean.PersonalTariff;<br /> import bitel.billing.server.contract.bean.PersonalTariffManager;<br /> import bitel.billing.server.dialup.bean.DialUpLoginManager;<br /> import bitel.billing.server.email.bean.Account;<br /> import bitel.billing.server.email.bean.AccountManager;<br /> import bitel.billing.server.npay.bean.ServiceObject;<br /> import bitel.billing.server.npay.bean.ServiceObjectManager;<br /> import bitel.billing.server.phone.bean.ClientItem;<br /> import bitel.billing.server.phone.bean.ClientItemManager;<br /> import bitel.billing.server.voiceip.bean.VoiceIpLoginManager;<br /> import org.w3c.dom.Element;<br /> import org.w3c.dom.NodeList;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.base.server.DefaultContext;<br /> import ru.bitel.bgbilling.kernel.container.security.server.ModuleAction;<br /> import ru.bitel.bgbilling.kernel.container.security.server.PermissionChecker;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalance;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalanceManager;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.BGModule;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.api.server.bean.InetServDao;<br /> import ru.bitel.bgbilling.modules.ipn.common.bean.AddressRange;<br /> import ru.bitel.bgbilling.modules.ipn.server.bean.AddressRangeManager;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.XMLUtils;<br /> import ru.bitel.common.worker.ThreadContext;<br /> <br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.Date;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class ActionContractInfo extends bitel.billing.server.contract.action.ActionContractInfo{<br /> @Override<br /> public void doAction() throws SQLException, BGException<br /> {<br /> super.doAction();<br /> <br /> Date now = new Date();<br /> <br /> //Удаляем модули<br /> Element modulesNode = XMLUtils.selectElement(rootNode, &quot;/data/info/modules&quot;);<br /> if(modulesNode!=null) {<br /> NodeList list = modulesNode.getChildNodes();<br /> while (modulesNode.hasChildNodes()){<br /> modulesNode.removeChild(list.item(0));<br /> }<br /> }<br /> //Количество сущностей модуля<br /> int itemCount;<br /> //Количество открытых датой сущностей модуля<br /> int openItemCount;<br /> InetServDao servDao;<br /> InetServ tree;<br /> ClientItemManager clientItemManager;<br /> DialUpLoginManager lm;<br /> ServiceObjectManager manager;<br /> AccountManager accountManager;<br /> AddressRangeManager arm;<br /> <br /> //Добавляем модули заново с доп данными:<br /> for (BGModule module : new ContractModuleManager(this.con).getContractModules(cid))<br /> {<br /> Element item = createElement(modulesNode, &quot;item&quot;);<br /> String className = module.getInstalledModule().getPackageServer() + &quot;.Module&quot;;<br /> ApplicationModule moduleClass = (ApplicationModule) Utils.newInstance(className, ApplicationModule.class);<br /> itemCount=-1;<br /> openItemCount=-1;<br /> //Inet<br /> if(moduleClass instanceof ru.bitel.bgbilling.modules.inet.api.server.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> servDao = new InetServDao(con, module.getId(), 0);<br /> tree = servDao.tree(this.cid);<br /> if(tree.getChildren()!=null){<br /> itemCount = tree.getChildren().size();<br /> for (InetServ inetServ : tree.getChildren()) {<br /> if(TimeUtils.dateBefore(now, inetServ.getDateTo()) || inetServ.getDateTo()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> servDao.recycle();<br /> }<br /> <br /> //Phone<br /> if(moduleClass instanceof bitel.billing.server.phone.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> clientItemManager = new ClientItemManager(con, module.getId());<br /> for (ClientItem clientItem : clientItemManager.getItemList(this.cid)) {<br /> itemCount++;<br /> if(TimeUtils.dateBefore(Calendar.getInstance(), clientItem.getDate2()) || clientItem.getDate2()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Dialup<br /> if(moduleClass instanceof bitel.billing.server.dialup.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> lm = new DialUpLoginManager(con, module.getId());<br /> for (Login login : lm.getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Npay<br /> if(moduleClass instanceof bitel.billing.server.npay.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> manager = new ServiceObjectManager(con, module.getId());<br /> for (ServiceObject so : manager.getServiceObjectList(this.cid, null)) {<br /> itemCount++;<br /> if(so.getDate2()==null || TimeUtils.dateBefore(now, so.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Email<br /> if(moduleClass instanceof bitel.billing.server.email.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> accountManager = new AccountManager(con, module.getId());<br /> for (Account acc : accountManager.getContractAccountList(this.cid)) {<br /> itemCount++;<br /> if(acc.getDate2()==null || TimeUtils.dateBefore(now, acc.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //IPN<br /> if(moduleClass instanceof bitel.billing.server.ipn.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> arm = new AddressRangeManager(con, module.getId());<br /> for (AddressRange range: arm.getContractRanges(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> for (AddressRange range: arm.getContractNets(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //VoiceIP<br /> if(moduleClass instanceof bitel.billing.server.voiceip.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> for (Login login : new VoiceIpLoginManager(con, module.getId()).getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> <br /> item.setAttribute(&quot;id&quot;, String.valueOf(module.getId()));<br /> if(itemCount&gt;=0 &amp;&amp; openItemCount&gt;=0){<br /> item.setAttribute(&quot;title&quot;, &quot;&lt;html&gt;&quot;+module.getTitle()+&quot; &lt;font style='color: grey;' size='2'&gt;[&quot;+openItemCount+&quot;/&quot;+itemCount+&quot;]&lt;/font&gt;&lt;/htmp&gt;&quot;);<br /> }else{<br /> item.setAttribute(&quot;title&quot;, module.getTitle());<br /> }<br /> item.setAttribute(&quot;package&quot;, module.getInstalledModule().getPackageClient());<br /> if (moduleClass != null)<br /> {<br /> item.setAttribute(&quot;status&quot;, moduleClass.getStatus(this.con, module.getId(), cid));<br /> }<br /> }<br /> <br /> Element tariff = XMLUtils.selectElement(rootNode, &quot;/data/info/tariff&quot;);<br /> //Удаляем все тарифы из списка - будем строить свой список<br /> if(tariff!=null){<br /> NodeList list = tariff.getChildNodes();<br /> while(tariff.hasChildNodes()){<br /> tariff.removeChild(list.item(0));<br /> }<br /> /*for(int i = 0;i&lt;list.getLength();i++){<br /> tariff.removeChild(list.item(i));<br /> }*/<br /> //Составляем свой список тарифов с красивостями<br /> Map&lt;String, String&gt; mapRequest = new HashMap&lt;String, String&gt;();<br /> mapRequest.put(&quot;action&quot;, &quot;PersonalTariffTable&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract.tariff&quot;);<br /> <br /> ModuleAction action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> for (PersonalTariff personalTariff : new PersonalTariffManager(this.con)<br /> .getPersonalTariffList(cid, new Date())) {<br /> addListItem(tariff, personalTariff.getId(), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;&quot; + &quot;ПТ: &quot; + personalTariff.getTitle()+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> mapRequest.clear();<br /> mapRequest.put(&quot;action&quot;, &quot;ContractTariffPlans&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract&quot;);<br /> <br /> action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> String query = &quot;SELECT t2.id, t2.title, group_concat(TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM ctc.value))) value, t1.date1 FROM contract_tariff AS t1 left join tariff_plan AS t2 on t1.tpid=t2.id left join custom_tariff_cost ctc on t1.id=ctc.contract_tariff_id &quot; +<br /> &quot; WHERE t1.cid=? AND emid=0 AND eid=0 AND ( isNull( date2 ) OR date2&gt;=CURDATE() ) group by t1.id&quot;;<br /> //AND (isNull( date1 ) OR date1&lt;=CURDATE())<br /> PreparedStatement ps = this.con.prepareStatement(query);<br /> ps.setInt(1, cid);<br /> ResultSet rs = ps.executeQuery();<br /> String value;<br /> Date dt1;<br /> String name;<br /> while (rs.next())<br /> {<br /> value = rs.getString(3);<br /> name = rs.getString(2);<br /> //21.12.2016 by semyon@dsi.ru - show future tariffs too, but with another color<br /> dt1=TimeUtils.convertSqlDateToDate(rs.getDate(4));<br /> if(TimeUtils.dateBefore(now,dt1)){<br /> name = &quot;&lt;font style='color: #cc99ff;'&gt;&quot;+name+&quot;&lt;/font&gt;&quot;;<br /> }<br /> if(value==null){<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&quot;+name+&quot;&lt;/html&gt;&quot;);<br /> }else{<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;*&lt;/font&gt; &quot;+name+&quot; &lt;font style='color: green;'&gt;(&quot;+value+&quot;)&lt;/font&gt;&lt;/html&gt;&quot; );<br /> }<br /> }<br /> rs.close();<br /> ps.close();<br /> }<br /> }<br /> <br /> //Подсвечиваем статус &quot;закрыт&quot;<br /> Element contractElement = XMLUtils.selectElement(rootNode, &quot;contract&quot;);<br /> if(contractElement!=null){<br /> if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;закрыт&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;закрыт&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;приостановлен&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: blue;'&gt;приостановлен&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;в отключении&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: #5F84B6;'&gt;в отключении&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> //Проверяем баланс и лимит на договоре - выделяем лимит красным, если баланс&lt;лимита<br /> if(contractElement!=null){<br /> ConvergenceBalanceManager convergenceBalanceManager = ConvergenceBalanceManager.getInstance();<br /> if (convergenceBalanceManager != null) {<br /> ConvergenceBalance balance = convergenceBalanceManager.getBalance((ThreadContext.get(DefaultContext.class)).getConnectionSet(), this.cid, System.currentTimeMillis());<br /> if(balance!=null &amp;&amp; !balance.isBalanceUnderLimit()){<br /> contractElement.setAttribute(&quot;limit&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;&quot;+contractElement.getAttribute(&quot;limit&quot;)+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> }<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> == Заключение ==<br /> <br /> У читателя, вероятно, возникает резонный вопрос: &quot;Где howto? Дай мне готовый файлик, чтобы пользоваться фичей!&quot;.<br /> Но пока &lt;s&gt;мне лень&lt;/s&gt; я не буду выкладывать готовых компилированных файлов и инструкций по подмене jar клиента и сервера.<br /> <br /> * Во-первых, это всё работает на 5.2 и на старших версиях не тестировалось - наверняка вам всё равно придётся многое переписывать.<br /> * Во-вторых, использовать подмену стандартных классов плохо для кармы - не хочу, чтобы этим массово пользовались<br /> * В-третьих, давайте сперва обсудим фичу на [https://forum.bitel.ru/viewtopic.php?f=66&amp;t=9709 форуме]<br /> <br /> Хотелось бы, чтобы что-то подобное появилось в стандартной реализации от Битела. Собственно, в первую очередь поэтому я и решил выложить решение спустя 4 года после внедрения :) <br /> <br /> Сейчас у меня в табличке custom_tariff_cost уже 5700 записей, фича работает как часы. Но есть минус - из-за большого количества таких фич в продакшене осложняется обновление на версии 6 и 7: нужно предварительно тестировать и переписывать большой объём кода.<br /> <br /> Если вам интересны персональные тарифы на договоре, &lt;s&gt;подписывайтесь, ставьте лайк&lt;/s&gt; пишите на форум разработчикам - чем больше будет запросов, тем быстрее появится стандартная реализация.<br /> <br /> Также, если будет интерес, могу выложить свою реализацию процентных скидок на услуги. Она менее &quot;костыльная&quot; - не требуется подмена классов, только npay.xml для собственных тарифных узлов.<br /> <br /> ps. Ещё можно скинуться автору на [https://money.yandex.ru/to/41001959637999 кофе] :)<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 08:00, 8 мая 2017 (UTC)</div> Mon, 08 May 2017 08:02:53 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Заключение */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> <br /> Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.<br /> <br /> ==== Веб-сервис переопределения цен ====<br /> <br /> Вспомогательные классы:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import ru.bitel.common.model.Id;<br /> <br /> import java.io.Serializable;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Тарифный план договора для использования в веб-сервисе.<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя<br /> *<br /> */<br /> public class ContractTariff extends Id implements Serializable {<br /> private int contractId;<br /> private int tariffPlanId;<br /> private Date date1;<br /> private Date date2;<br /> private String comment = &quot;&quot;;<br /> private String tariffName;<br /> private int pos;<br /> private int entityMid;<br /> private int entityId;<br /> private int replacedFromContractTariffId;<br /> <br /> public ContractTariff() {<br /> this.id = -1;<br /> this.contractId = -1;<br /> this.tariffPlanId = -1;<br /> this.date1 = null;<br /> this.date2 = null;<br /> this.tariffName = null;<br /> this.pos = 0;<br /> this.entityId = 0;<br /> this.entityMid = 0;<br /> this.replacedFromContractTariffId = -1;<br /> }<br /> <br /> public int getContractId() {<br /> return contractId;<br /> }<br /> <br /> public void setContractId(int contractId) {<br /> this.contractId = contractId;<br /> }<br /> <br /> public int getTariffPlanId() {<br /> return tariffPlanId;<br /> }<br /> <br /> public String getTariffName() {<br /> return tariffName;<br /> }<br /> <br /> public void setTariffName(String tariffName) {<br /> this.tariffName = tariffName;<br /> }<br /> <br /> public void setTariffPlanId(int tariffPlanId) {<br /> this.tariffPlanId = tariffPlanId;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> <br /> public String getComment() {<br /> return comment;<br /> }<br /> <br /> public void setComment(String comment) {<br /> this.comment = comment;<br /> }<br /> <br /> public int getPos() {<br /> return pos;<br /> }<br /> <br /> public void setPos(int pos) {<br /> this.pos = pos;<br /> }<br /> <br /> public int getEntityMid() {<br /> return entityMid;<br /> }<br /> <br /> public void setEntityMid(int entityMid) {<br /> this.entityMid = entityMid;<br /> }<br /> <br /> public int getEntityId() {<br /> return entityId;<br /> }<br /> <br /> public void setEntityId(int entityId) {<br /> this.entityId = entityId;<br /> }<br /> <br /> public int getReplacedFromContractTariffId() {<br /> return replacedFromContractTariffId;<br /> }<br /> <br /> public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {<br /> this.replacedFromContractTariffId = replacedFromContractTariffId;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом<br /> *<br /> */<br /> @WebService<br /> @XmlSeeAlso({ContractTariff.class})<br /> public abstract interface ContractTariffService {<br /> @WebMethod<br /> public abstract List&lt;ContractTariff&gt; contractTariffList(@WebParam(name=&quot;contractId&quot;) int contracId) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.util.List;<br /> <br /> /**<br /> * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService&quot;)<br /> public class ContractTariffServiceImpl extends AbstractService<br /> implements ContractTariffService {<br /> <br /> @Override<br /> public List&lt;ContractTariff&gt; contractTariffList(@WebParam(name = &quot;contractId&quot;) int contracId) throws BGException {<br /> return (new ContractTariffDao(getConnection()).list(contracId));<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Веб-сервис работы с переопределёнными ценами:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Интерфейс веб-сервиса для работы с персональными ценами<br /> */<br /> @WebService<br /> @XmlSeeAlso({CustomTariffCost.class})<br /> public abstract interface CustomTariffCostService {<br /> <br /> @WebMethod<br /> public abstract List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id)<br /> throws BGException;<br /> <br /> @WebMethod<br /> public abstract void updateCustomTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs)<br /> throws BGException;<br /> <br /> /**<br /> * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> */<br /> @WebMethod<br /> public abstract Set&lt;Integer&gt; availableServiceList(@WebParam(name=&quot;tariffPlanId&quot;) int tpid) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;<br /> import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;<br /> import ru.bitel.bgbilling.server.util.ClosedDateChecker;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.List;<br /> import java.util.Set;<br /> import java.util.TreeSet;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * Реализация веб-сервиса для работы с персональными ценам<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService&quot;)<br /> public class CustomTariffCostServiceImpl extends AbstractService<br /> implements CustomTariffCostService {<br /> <br /> @Override<br /> public List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name = &quot;contractTariffId&quot;) int contract_tariff_id) throws BGException {<br /> return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);<br /> }<br /> <br /> @Override<br /> public void updateCustomTariffCostList(@WebParam(name = &quot;contractTariffId&quot;)int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs) throws BGException{<br /> <br /> Calendar closedDate;<br /> ContractTariffManager ctm = new ContractTariffManager(getConnection());<br /> ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);<br /> if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking(&quot;updateCustomTariffCostList&quot;, 0, this.userId)) != null)<br /> {<br /> ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());<br /> }<br /> CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());<br /> try {<br /> ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);<br /> } catch (SQLException e) {<br /> throw new BGException(e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> * На данный момент реализовано через метки - ищем все метки тарифного плана вида &quot;id - Название&quot;,<br /> * где id - это id услуги. Например &quot;5 - Интернет&quot;.<br /> */<br /> @Override<br /> public Set&lt;Integer&gt; availableServiceList(@WebParam(name = &quot;tariffPlanId&quot;) int tpid)<br /> throws BGException<br /> {<br /> Set&lt;Integer&gt; result = new TreeSet&lt;Integer&gt;();<br /> TariffLabelManager tlm = new TariffLabelManager(getConnection());<br /> //Получаем список id меток тарифа<br /> Set&lt;Integer&gt; tariffLabelIds = tlm.getTariffLabelIds(tpid);<br /> if(tariffLabelIds.size()&gt;0){<br /> //Берём список всех меток с названиями<br /> List&lt;TariffLabelItem&gt; tariffLabelItemList = tlm.getTariffLabelItemList();<br /> //Ищем среди меток тарифа метки с названиями вида &quot;id - Название&quot;<br /> /*for(Integer label_id : tariffLabelIds){<br /> TariffLabelItem label = tariffLabelItemList.get()<br /> }*/<br /> Pattern pattern = Pattern.compile(&quot;^(\\d+) - .+$&quot;);<br /> <br /> Matcher matcher;<br /> //Перебираем все метки из справочника<br /> for(TariffLabelItem label : tariffLabelItemList){<br /> //Если метка есть на нашем тарифе ...<br /> if(tariffLabelIds.contains(label.getId())){<br /> //...то смотрим её название<br /> matcher = pattern.matcher(label.getTitle());<br /> if(matcher.find()){//определился sid услуги - отлично, добавляем в результат<br /> try{<br /> result.add(Integer.valueOf(matcher.group(1)));<br /> }catch (Exception e){}<br /> }<br /> }<br /> }<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> ==== Клиетский интерфейс ====<br /> <br /> Для начала немного костылей.<br /> <br /> Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:<br /> <br /> [[Файл:custom_cost_labels.jpg]]<br /> <br /> Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида &quot;xxx - Наименование&quot;, где xxx - sid услуги, дополненный нулями до 3 символов, и проставить эту метку для всех необходимых тарифов.<br /> <br /> Переходим собственно к интерфейсу.<br /> <br /> Менять клиентский интерфейс биллинга стандартным образом нельзя - только через переопределение jar-файлов клиента.<br /> <br /> Я долго думал, куда бы запихать управление персональными ценами малой кровью.<br /> По-идее, будь я разработчиком Битела, я бы сделал это в ядре (т.к. в общем случае можно реализовать переопределение не только абонплат). Соответственно, редактирование персональных цен должно быть где-то в разделе &quot;Тарифные планы&quot; договора - например, отдельной вкладкой после &quot;Персональных тарифов&quot; и &quot;Тарифных опций&quot;.<br /> Но малой кровью модифицировать эту клиентскую панель не получилось, поэтому было решено сделать отдельную вкладку &quot;Переопределение цен&quot; на странице модуля Npay - см http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost.jpg<br /> <br /> <br /> Привожу клиентский код для 5.2<br /> Что с ним делать - решайте сами на свой страх и риск :)<br /> <br /> Добавляем нашу панель - прописываем в клиентский файл bitel/billing/module/services/npay/setup_user.properties:<br /> <br /> &lt;source lang=ini&gt;<br /> module.tab.3.class=bitel.billing.module.services.npay.CustomCostModulePanel<br /> module.tab.3.title=\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0446\u0435\u043D<br /> &lt;/source&gt;<br /> <br /> Панель:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.module.common.BGControlPanelContractSelect;<br /> import bitel.billing.module.common.BGTitleBorder;<br /> import bitel.billing.module.common.DialogToolBar;<br /> import bitel.billing.module.common.FloatTextField;<br /> import ru.bitel.bgbilling.client.common.BGEditor;<br /> import ru.bitel.bgbilling.client.common.BGUPanel;<br /> import ru.bitel.bgbilling.client.common.BGUTable;<br /> import ru.bitel.bgbilling.client.common.ClientContext;<br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.Service;<br /> import ru.bitel.bgbilling.kernel.module.common.service.ServiceService;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.client.AbstractBGUPanel;<br /> import ru.bitel.common.client.BGSwingUtilites;<br /> import ru.bitel.common.client.BGUComboBox;<br /> import ru.bitel.common.client.table.BGTableModel;<br /> import ru.bitel.common.model.KeyValue;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.swing.*;<br /> import java.awt.*;<br /> import java.awt.event.ActionEvent;<br /> import java.awt.event.ActionListener;<br /> import java.math.BigDecimal;<br /> import java.util.*;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Панелька на клиенте для редактирования переопределений стоимости тарифных планов договоров.<br /> */<br /> public class CustomCostModulePanel extends BGUPanel<br /> {<br /> private BGControlPanelContractSelect contractSelect;<br /> private ContractTariffService wsContractTariff = getContext().getPort(ContractTariffService.class, 0);<br /> <br /> private BGTableModel&lt;ContractTariff&gt; model = new BGTableModel&lt;ContractTariff&gt;(&quot;contractTariff&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, Integer.class, 0, 0, 0, &quot;id&quot;, true, false);<br /> addColumn(&quot;TPID&quot;, Integer.class, 0, 0, 0, &quot;tpid&quot;, true, false);<br /> addColumn(&quot;Название&quot;, &quot;tariffName&quot;, true);<br /> addColumn(&quot;Период&quot;, &quot;period&quot;, true);<br /> addColumn(&quot;Комментарий&quot;, &quot;comment&quot;, false);<br /> addColumn(&quot;Поз.&quot;, &quot;pos&quot;, true);<br /> }<br /> <br /> @Override<br /> public Object getValue(ContractTariff val, int column)<br /> {<br /> Object result = &quot;&quot;;<br /> switch (column)<br /> {<br /> case 3:<br /> result = TimeUtils.formatPeriod(val.getDate1(), val.getDate2());<br /> break;<br /> default:<br /> result = super.getValue(val, column);<br /> }<br /> return result;<br /> }<br /> };<br /> <br /> public CustomCostModulePanel()<br /> {<br /> super(new BorderLayout());<br /> //setLayout(new GridBagLayout());<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;refresh&quot;, &quot;Обновить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostModulePanel.this.setData();<br /> }<br /> };<br /> }<br /> <br /> public void setData() throws BGException {<br /> String cids = CustomCostModulePanel.this.contractSelect.getContracts();<br /> int cid = -1;<br /> try{<br /> cid = Utils.toIntegerList(cids).get(0);<br /> }catch(Exception ex){<br /> this.processException(ex);<br /> }<br /> <br /> CustomCostModulePanel.this.model.setData(CustomCostModulePanel.this.wsContractTariff.contractTariffList(cid));<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> setLayout(new GridBagLayout());<br /> BGUTable table = new BGUTable(this.model);<br /> <br /> JButton okButton = new JButton(&quot;OK&quot;);<br /> okButton.setMargin(new Insets(2, 2, 2, 2));<br /> okButton.addActionListener(new ActionListener() {<br /> public void actionPerformed(ActionEvent e) {<br /> try {<br /> CustomCostModulePanel.this.setData();<br /> } catch (BGException e1) {<br /> processException(e1);<br /> }<br /> }<br /> });<br /> this.contractSelect = new BGControlPanelContractSelect(true, true);<br /> this.contractSelect.add(okButton, new GridBagConstraints(2, 0, 1, 1, 0.0D, 0.0D, 10, 0,<br /> new Insets(0, 0, 5, 5), 0, 0));<br /> <br /> add(BGSwingUtilites.wrapBorder(this.contractSelect, &quot;Выберите договор&quot;), new GridBagConstraints(0, 0, 1, 1, 1.0D, 0.0D, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.setVisible(false);<br /> editor.addForm(new CustomCostTariffPanel(getContext()));<br /> add(BGSwingUtilites.wrapBorder(new JScrollPane(table), &quot;Тарифные планы договора&quot;),<br /> new GridBagConstraints(0, 1, 1, 1, 1.0D, 0.6D, 10, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 1.0D, GridBagConstraints.SOUTH, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> BGSwingUtilites.handleEdit(table, editor);<br /> }<br /> <br /> /**<br /> * Панель редактирования собственно списка переопределений услуг конкретного тарифа договора<br /> */<br /> public class CustomCostTariffPanel extends BGUPanel {<br /> private BGUTable table;<br /> CustomTariffCostService wsCustomCost = this.getContext().getPort(CustomTariffCostService.class, 0);<br /> private Map&lt;Integer, Service&gt; servicesMap;//Справочник услуг всех модулей : sid -&gt; Service<br /> private ContractTariff current;<br /> <br /> private BGTableModel&lt;CustomCostRow&gt; model = new BGTableModel&lt;CustomCostRow&gt;(&quot;customCosts&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, 0, 0, 0, null, true);<br /> addColumn(&quot;Услуга&quot;, -1, 120, -1, null, false);<br /> addColumn(&quot;Цена&quot;, BigDecimal.class, -1, 180, -1, null, false);<br /> }<br /> <br /> public Object getValue(CustomCostRow val, int column)<br /> {<br /> switch (column)<br /> {<br /> case 0:<br /> return val.sid;<br /> case 1:<br /> return val.title;<br /> case 2:<br /> return val.value;<br /> default:<br /> return super.getValue(val, column);<br /> }<br /> }<br /> };<br /> <br /> protected void initActions(){<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if ((CustomCostTariffPanel.this.current = CustomCostModulePanel.this.model.getSelectedRow()) != null)<br /> {<br /> CustomCostTariffPanel.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;Сохранить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if(CustomCostTariffPanel.this.current!=null){<br /> //CustomCostTariffPanel.this.model.getRows();<br /> List&lt;CustomTariffCost&gt; costs = new ArrayList&lt;CustomTariffCost&gt;();<br /> if(CustomCostTariffPanel.this.model.getRows()!=null){<br /> CustomTariffCost cost;<br /> for(CustomCostRow row : CustomCostTariffPanel.this.model.getRows()){<br /> cost = new CustomTariffCost(CustomCostTariffPanel.this.current.getId(),row.sid,row.value);<br /> costs.add(cost);<br /> }<br /> }<br /> CustomCostTariffPanel.this.wsCustomCost.updateCustomTariffCostList(<br /> CustomCostTariffPanel.this.current.getId(),<br /> costs<br /> );<br /> }<br /> CustomCostTariffPanel.this.performActionClose();<br /> }<br /> };<br /> }<br /> <br /> @Override<br /> public void performActionOpen()<br /> {<br /> //расставляем данные в форме редактирования, затем отображаем её<br /> setData(this.current.getId(), this.current.getTariffPlanId());<br /> super.performActionOpen();<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> //To change body of implemented methods use File | Settings | File Templates.<br /> setLayout(new GridBagLayout());<br /> this.table = new BGUTable(this.model);<br /> DialogToolBar toolBar = new DialogToolBar();<br /> <br /> ParamForm paramForm = new ParamForm(getContext());<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.addForm(paramForm);<br /> editor.setVisible(false);<br /> <br /> BGSwingUtilites.buildToolBar(toolBar, paramForm);<br /> toolBar.compact();<br /> <br /> BGSwingUtilites.handleEdit(this.table, editor);<br /> <br /> add(toolBar, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 0), 0, 0));<br /> add(new JScrollPane(this.table), new GridBagConstraints(0, 1, 1, 1, 1.0D, 1.0D, 10, 1, new Insets(0, 0,<br /> 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(5, 5, 5, 5), 0, 0));<br /> }<br /> <br /> //Сравниваем строчки цен для сортировки в таблице<br /> private final Comparator&lt;CustomCostRow&gt; COMPARATOR = new Comparator&lt;CustomCostRow&gt;()<br /> {<br /> public int compare(CustomCostRow o1, CustomCostRow o2)<br /> {<br /> int result = Integer.valueOf(o1.sid).compareTo(o2.sid);<br /> if (result != 0)<br /> {<br /> return result;<br /> }<br /> <br /> return o1.title.compareTo(o2.title);<br /> }<br /> };<br /> <br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> private BGUComboBox&lt;KeyValue&lt;Integer, String&gt;&gt; keyCombo = new BGUComboBox(<br /> KeyValue.class);<br /> <br /> public CustomCostTariffPanel(ClientContext context)<br /> {<br /> super(context);<br /> //Кэшируем список всех услуг биллинга. Если возникнет ошибка при его получении - ругаемся<br /> try {<br /> java.util.List&lt;Service&gt; serviceList = CustomCostTariffPanel.this.getContext().getPort(ServiceService.class, 0).serviceList(0);<br /> this.servicesMap = new HashMap&lt;Integer, Service&gt;(serviceList.size());<br /> for(Service s : serviceList){<br /> this.servicesMap.put(s.getId(),s);<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> }<br /> }<br /> <br /> public void setData(int id, int tpid){<br /> java.util.List&lt;CustomTariffCost&gt; costList;<br /> try {<br /> costList = wsCustomCost.customTariffCostList(id);<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> java.util.List&lt;CustomCostRow&gt; data = new ArrayList&lt;CustomCostRow&gt;(costList.size());<br /> String title;<br /> Service s;<br /> for(CustomTariffCost cost : costList){<br /> s = this.servicesMap.get(cost.getSid());<br /> if(s!=null){<br /> title = s.getTitle();<br /> }else{<br /> title = String.valueOf(cost.getSid());<br /> }<br /> data.add(new CustomCostRow(cost.getSid(), title, cost.getValue()));<br /> }<br /> Collections.sort(data, COMPARATOR);<br /> this.model.setData(data);<br /> <br /> //Получаем для комбобокса список услуг, для которых разрешено переопределение на этом тарифе<br /> java.util.List&lt;KeyValue&lt;Integer, String&gt;&gt; typeList = new ArrayList&lt;KeyValue&lt;Integer, String&gt;&gt;(CustomCostTariffPanel.this.servicesMap.size());<br /> try{<br /> for(Integer sid : this.wsCustomCost.availableServiceList(tpid)){<br /> s = this.servicesMap.get(sid);<br /> if(null!=s){<br /> title = s.getTitle();<br /> }else{<br /> title = &quot;&lt;&quot;+sid+&quot;&gt;&quot;;<br /> }<br /> <br /> typeList.add(new KeyValue&lt;Integer, String&gt;(sid, title));<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> CustomCostTariffPanel.this.keyCombo.setData(typeList);<br /> }<br /> <br /> class CustomCostRow<br /> {<br /> public int sid;<br /> public String title;<br /> public BigDecimal value;<br /> <br /> public CustomCostRow(int sid, String title, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.title = title;<br /> this.value = value;<br /> }<br /> <br /> /*public CustomCostRow(int sid, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.value = value;<br /> }*/<br /> }<br /> <br /> class ParamForm extends BGUPanel<br /> {<br /> private FloatTextField value = new FloatTextField();<br /> private CustomCostRow current;<br /> private boolean edit;<br /> <br /> public ParamForm(ClientContext context)<br /> {<br /> super(context);<br /> }<br /> <br /> protected void jbInit()<br /> {<br /> setBorder(BorderFactory.createCompoundBorder(new BGTitleBorder(&quot; Редактор &quot;), BorderFactory.createEmptyBorder(0, 3, 3, 3)));<br /> <br /> add(CustomCostTariffPanel.this.keyCombo, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 5), 0, 0));<br /> <br /> add(this.value, new GridBagConstraints(1, 0, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(0, 0, 0, 0),<br /> 0, 0));<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;new&quot;, &quot;Добавить&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = false;<br /> CustomCostTariffPanel.ParamForm.this.current = new CustomCostRow(0,&quot;&quot;,BigDecimal.ZERO);<br /> <br /> CustomCostTariffPanel.ParamForm.this.value.setText(&quot;&quot;);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(true);<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = true;<br /> <br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.keyCombo.setSelectedItem(ParamForm.this.current.sid);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(false);<br /> <br /> ParamForm.this.value.setText(String.valueOf(ParamForm.this.current.value));<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;delete&quot;, &quot;Удалить&quot;, ClientUtils.getIcon(&quot;item_delete&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> <br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.model.deleteSelectedRow();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;OK&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> KeyValue&lt;Integer, String&gt; key = CustomCostTariffPanel.this.keyCombo.getSelectedItem();<br /> if (key == null)<br /> {<br /> return;<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.current.sid = key.getKey();<br /> CustomCostTariffPanel.ParamForm.this.current.title = key.getValue();<br /> CustomCostTariffPanel.ParamForm.this.current.value = Utils.parseBigDecimal(CustomCostTariffPanel.ParamForm.this.value.getText(), BigDecimal.ZERO);<br /> <br /> if (!CustomCostTariffPanel.ParamForm.this.edit)<br /> {<br /> CustomCostTariffPanel.this.model.addRow(CustomCostTariffPanel.ParamForm.this.current);<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionClose();<br /> }<br /> };<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> Клиентские узлы тарифа:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел DayModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;за день&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;за месяц&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;за день&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;за месяц&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел MonthModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> <br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;безусловно&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;пропорц. периоду&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;2&quot;, &quot;пропорц. объему&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;3&quot;, &quot;как выгоднее&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;безусловно&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;пропорц. периоду&quot;);<br /> }<br /> else if (this.type.equals(&quot;2&quot;))<br /> {<br /> title.append(&quot;пропорц. объему&quot;);<br /> }<br /> else if (this.type.equals(&quot;3&quot;))<br /> {<br /> title.append(&quot;как выгоднее&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел YearModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Стоимость: &quot;));<br /> edit.add(this.costTf);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(&quot;Стоимость: &quot;);<br /> title.append(this.cost);<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.custom = Utils.parseBoolean((String) data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> this.costTf.setText(this.cost);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> === Отображение на договоре ===<br /> <br /> На договоре персональные цены на услуги будем писать другим шрифтом в скобках через запятую после наименования тарифа в дереве.<br /> <br /> Для этого переопределим через dynaction стандартный action вывода карточки договора:<br /> <br /> &lt;source lang=ini&gt;<br /> dynaction:contract.hierarchy.ActionContractInfo=ru.dsi.bgbilling.kernel.scripts.action.ActionContractInfo<br /> &lt;/source&gt;<br /> <br /> Переопределённый action делает много всяких штук, кроме подсветки персональных цен. Привожу целиком, лишнее можно убрать по необходимости:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.scripts.action.contract;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.ApplicationModule;<br /> import bitel.billing.server.call.bean.Login;<br /> import bitel.billing.server.contract.bean.ContractModuleManager;<br /> import bitel.billing.server.contract.bean.PersonalTariff;<br /> import bitel.billing.server.contract.bean.PersonalTariffManager;<br /> import bitel.billing.server.dialup.bean.DialUpLoginManager;<br /> import bitel.billing.server.email.bean.Account;<br /> import bitel.billing.server.email.bean.AccountManager;<br /> import bitel.billing.server.npay.bean.ServiceObject;<br /> import bitel.billing.server.npay.bean.ServiceObjectManager;<br /> import bitel.billing.server.phone.bean.ClientItem;<br /> import bitel.billing.server.phone.bean.ClientItemManager;<br /> import bitel.billing.server.voiceip.bean.VoiceIpLoginManager;<br /> import org.w3c.dom.Element;<br /> import org.w3c.dom.NodeList;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.base.server.DefaultContext;<br /> import ru.bitel.bgbilling.kernel.container.security.server.ModuleAction;<br /> import ru.bitel.bgbilling.kernel.container.security.server.PermissionChecker;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalance;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalanceManager;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.BGModule;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.api.server.bean.InetServDao;<br /> import ru.bitel.bgbilling.modules.ipn.common.bean.AddressRange;<br /> import ru.bitel.bgbilling.modules.ipn.server.bean.AddressRangeManager;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.XMLUtils;<br /> import ru.bitel.common.worker.ThreadContext;<br /> <br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.Date;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class ActionContractInfo extends bitel.billing.server.contract.action.ActionContractInfo{<br /> @Override<br /> public void doAction() throws SQLException, BGException<br /> {<br /> super.doAction();<br /> <br /> Date now = new Date();<br /> <br /> //Удаляем модули<br /> Element modulesNode = XMLUtils.selectElement(rootNode, &quot;/data/info/modules&quot;);<br /> if(modulesNode!=null) {<br /> NodeList list = modulesNode.getChildNodes();<br /> while (modulesNode.hasChildNodes()){<br /> modulesNode.removeChild(list.item(0));<br /> }<br /> }<br /> //Количество сущностей модуля<br /> int itemCount;<br /> //Количество открытых датой сущностей модуля<br /> int openItemCount;<br /> InetServDao servDao;<br /> InetServ tree;<br /> ClientItemManager clientItemManager;<br /> DialUpLoginManager lm;<br /> ServiceObjectManager manager;<br /> AccountManager accountManager;<br /> AddressRangeManager arm;<br /> <br /> //Добавляем модули заново с доп данными:<br /> for (BGModule module : new ContractModuleManager(this.con).getContractModules(cid))<br /> {<br /> Element item = createElement(modulesNode, &quot;item&quot;);<br /> String className = module.getInstalledModule().getPackageServer() + &quot;.Module&quot;;<br /> ApplicationModule moduleClass = (ApplicationModule) Utils.newInstance(className, ApplicationModule.class);<br /> itemCount=-1;<br /> openItemCount=-1;<br /> //Inet<br /> if(moduleClass instanceof ru.bitel.bgbilling.modules.inet.api.server.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> servDao = new InetServDao(con, module.getId(), 0);<br /> tree = servDao.tree(this.cid);<br /> if(tree.getChildren()!=null){<br /> itemCount = tree.getChildren().size();<br /> for (InetServ inetServ : tree.getChildren()) {<br /> if(TimeUtils.dateBefore(now, inetServ.getDateTo()) || inetServ.getDateTo()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> servDao.recycle();<br /> }<br /> <br /> //Phone<br /> if(moduleClass instanceof bitel.billing.server.phone.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> clientItemManager = new ClientItemManager(con, module.getId());<br /> for (ClientItem clientItem : clientItemManager.getItemList(this.cid)) {<br /> itemCount++;<br /> if(TimeUtils.dateBefore(Calendar.getInstance(), clientItem.getDate2()) || clientItem.getDate2()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Dialup<br /> if(moduleClass instanceof bitel.billing.server.dialup.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> lm = new DialUpLoginManager(con, module.getId());<br /> for (Login login : lm.getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Npay<br /> if(moduleClass instanceof bitel.billing.server.npay.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> manager = new ServiceObjectManager(con, module.getId());<br /> for (ServiceObject so : manager.getServiceObjectList(this.cid, null)) {<br /> itemCount++;<br /> if(so.getDate2()==null || TimeUtils.dateBefore(now, so.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Email<br /> if(moduleClass instanceof bitel.billing.server.email.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> accountManager = new AccountManager(con, module.getId());<br /> for (Account acc : accountManager.getContractAccountList(this.cid)) {<br /> itemCount++;<br /> if(acc.getDate2()==null || TimeUtils.dateBefore(now, acc.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //IPN<br /> if(moduleClass instanceof bitel.billing.server.ipn.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> arm = new AddressRangeManager(con, module.getId());<br /> for (AddressRange range: arm.getContractRanges(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> for (AddressRange range: arm.getContractNets(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //VoiceIP<br /> if(moduleClass instanceof bitel.billing.server.voiceip.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> for (Login login : new VoiceIpLoginManager(con, module.getId()).getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> <br /> item.setAttribute(&quot;id&quot;, String.valueOf(module.getId()));<br /> if(itemCount&gt;=0 &amp;&amp; openItemCount&gt;=0){<br /> item.setAttribute(&quot;title&quot;, &quot;&lt;html&gt;&quot;+module.getTitle()+&quot; &lt;font style='color: grey;' size='2'&gt;[&quot;+openItemCount+&quot;/&quot;+itemCount+&quot;]&lt;/font&gt;&lt;/htmp&gt;&quot;);<br /> }else{<br /> item.setAttribute(&quot;title&quot;, module.getTitle());<br /> }<br /> item.setAttribute(&quot;package&quot;, module.getInstalledModule().getPackageClient());<br /> if (moduleClass != null)<br /> {<br /> item.setAttribute(&quot;status&quot;, moduleClass.getStatus(this.con, module.getId(), cid));<br /> }<br /> }<br /> <br /> Element tariff = XMLUtils.selectElement(rootNode, &quot;/data/info/tariff&quot;);<br /> //Удаляем все тарифы из списка - будем строить свой список<br /> if(tariff!=null){<br /> NodeList list = tariff.getChildNodes();<br /> while(tariff.hasChildNodes()){<br /> tariff.removeChild(list.item(0));<br /> }<br /> /*for(int i = 0;i&lt;list.getLength();i++){<br /> tariff.removeChild(list.item(i));<br /> }*/<br /> //Составляем свой список тарифов с красивостями<br /> Map&lt;String, String&gt; mapRequest = new HashMap&lt;String, String&gt;();<br /> mapRequest.put(&quot;action&quot;, &quot;PersonalTariffTable&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract.tariff&quot;);<br /> <br /> ModuleAction action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> for (PersonalTariff personalTariff : new PersonalTariffManager(this.con)<br /> .getPersonalTariffList(cid, new Date())) {<br /> addListItem(tariff, personalTariff.getId(), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;&quot; + &quot;ПТ: &quot; + personalTariff.getTitle()+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> mapRequest.clear();<br /> mapRequest.put(&quot;action&quot;, &quot;ContractTariffPlans&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract&quot;);<br /> <br /> action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> String query = &quot;SELECT t2.id, t2.title, group_concat(TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM ctc.value))) value, t1.date1 FROM contract_tariff AS t1 left join tariff_plan AS t2 on t1.tpid=t2.id left join custom_tariff_cost ctc on t1.id=ctc.contract_tariff_id &quot; +<br /> &quot; WHERE t1.cid=? AND emid=0 AND eid=0 AND ( isNull( date2 ) OR date2&gt;=CURDATE() ) group by t1.id&quot;;<br /> //AND (isNull( date1 ) OR date1&lt;=CURDATE())<br /> PreparedStatement ps = this.con.prepareStatement(query);<br /> ps.setInt(1, cid);<br /> ResultSet rs = ps.executeQuery();<br /> String value;<br /> Date dt1;<br /> String name;<br /> while (rs.next())<br /> {<br /> value = rs.getString(3);<br /> name = rs.getString(2);<br /> //21.12.2016 by semyon@dsi.ru - show future tariffs too, but with another color<br /> dt1=TimeUtils.convertSqlDateToDate(rs.getDate(4));<br /> if(TimeUtils.dateBefore(now,dt1)){<br /> name = &quot;&lt;font style='color: #cc99ff;'&gt;&quot;+name+&quot;&lt;/font&gt;&quot;;<br /> }<br /> if(value==null){<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&quot;+name+&quot;&lt;/html&gt;&quot;);<br /> }else{<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;*&lt;/font&gt; &quot;+name+&quot; &lt;font style='color: green;'&gt;(&quot;+value+&quot;)&lt;/font&gt;&lt;/html&gt;&quot; );<br /> }<br /> }<br /> rs.close();<br /> ps.close();<br /> }<br /> }<br /> <br /> //Подсвечиваем статус &quot;закрыт&quot;<br /> Element contractElement = XMLUtils.selectElement(rootNode, &quot;contract&quot;);<br /> if(contractElement!=null){<br /> if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;закрыт&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;закрыт&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;приостановлен&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: blue;'&gt;приостановлен&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;в отключении&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: #5F84B6;'&gt;в отключении&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> //Проверяем баланс и лимит на договоре - выделяем лимит красным, если баланс&lt;лимита<br /> if(contractElement!=null){<br /> ConvergenceBalanceManager convergenceBalanceManager = ConvergenceBalanceManager.getInstance();<br /> if (convergenceBalanceManager != null) {<br /> ConvergenceBalance balance = convergenceBalanceManager.getBalance((ThreadContext.get(DefaultContext.class)).getConnectionSet(), this.cid, System.currentTimeMillis());<br /> if(balance!=null &amp;&amp; !balance.isBalanceUnderLimit()){<br /> contractElement.setAttribute(&quot;limit&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;&quot;+contractElement.getAttribute(&quot;limit&quot;)+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> }<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> == Заключение ==<br /> <br /> У читателя, вероятно, возникает резонный вопрос: &quot;Где howto? Дай мне готовый файлик, чтобы пользоваться фичей!&quot;.<br /> Но пока &lt;s&gt;мне лень&lt;/s&gt; я не буду выкладывать готовых компилированных файлов и инструкций по подмене jar клиента и сервера.<br /> <br /> * Во-первых, это всё работает на 5.2 и на старших версиях не тестировалось - наверняка вам всё равно придётся многое переписывать.<br /> * Во-вторых, использовать подмену стандартных классов плохо для кармы - не хочу, чтобы этим массово пользовались<br /> * В-третьих, давайте сперва обсудим фичу на [https://forum.bitel.ru/viewtopic.php?f=66&amp;t=9709 форуме]<br /> <br /> Хотелось бы, чтобы что-то подобное появилось в стандартной реализации от Битела. Собственно, в первую очередь поэтому я и решил выложить решение спустя 4 года после внедрения :) <br /> <br /> Сейчас у меня в табличке custom_tariff_cost уже 5700 записей, фича работает как часы. Но есть минус - из-за большого количества таких фич в продакшене осложняется обновление на версии 6 и 7: нужно предварительно тестировать и переписывать большой объём кода.<br /> <br /> Если вам интересны персональные тарифы на договоре, &lt;s&gt;подписывайтесь, ставьте лайк&lt;/s&gt; пишите на форум разработчикам - чем больше будет запросов, тем быстрее появится стандартная реализация.<br /> <br /> Также, если будет интерес, могу выложить свою реализацию процентных скидок на услуги. Она менее &quot;костыльная&quot; - не требуется подмена классов, только npay.xml для собственных тарифных узлов.<br /> <br /> ps. Ещё можно скинуться автору на [https://money.yandex.ru/to/41001959637999 кофе] :)<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 08:00, 8 мая 2017 (UTC)</div> Mon, 08 May 2017 08:01:19 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Заключение */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> <br /> Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.<br /> <br /> ==== Веб-сервис переопределения цен ====<br /> <br /> Вспомогательные классы:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import ru.bitel.common.model.Id;<br /> <br /> import java.io.Serializable;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Тарифный план договора для использования в веб-сервисе.<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя<br /> *<br /> */<br /> public class ContractTariff extends Id implements Serializable {<br /> private int contractId;<br /> private int tariffPlanId;<br /> private Date date1;<br /> private Date date2;<br /> private String comment = &quot;&quot;;<br /> private String tariffName;<br /> private int pos;<br /> private int entityMid;<br /> private int entityId;<br /> private int replacedFromContractTariffId;<br /> <br /> public ContractTariff() {<br /> this.id = -1;<br /> this.contractId = -1;<br /> this.tariffPlanId = -1;<br /> this.date1 = null;<br /> this.date2 = null;<br /> this.tariffName = null;<br /> this.pos = 0;<br /> this.entityId = 0;<br /> this.entityMid = 0;<br /> this.replacedFromContractTariffId = -1;<br /> }<br /> <br /> public int getContractId() {<br /> return contractId;<br /> }<br /> <br /> public void setContractId(int contractId) {<br /> this.contractId = contractId;<br /> }<br /> <br /> public int getTariffPlanId() {<br /> return tariffPlanId;<br /> }<br /> <br /> public String getTariffName() {<br /> return tariffName;<br /> }<br /> <br /> public void setTariffName(String tariffName) {<br /> this.tariffName = tariffName;<br /> }<br /> <br /> public void setTariffPlanId(int tariffPlanId) {<br /> this.tariffPlanId = tariffPlanId;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> <br /> public String getComment() {<br /> return comment;<br /> }<br /> <br /> public void setComment(String comment) {<br /> this.comment = comment;<br /> }<br /> <br /> public int getPos() {<br /> return pos;<br /> }<br /> <br /> public void setPos(int pos) {<br /> this.pos = pos;<br /> }<br /> <br /> public int getEntityMid() {<br /> return entityMid;<br /> }<br /> <br /> public void setEntityMid(int entityMid) {<br /> this.entityMid = entityMid;<br /> }<br /> <br /> public int getEntityId() {<br /> return entityId;<br /> }<br /> <br /> public void setEntityId(int entityId) {<br /> this.entityId = entityId;<br /> }<br /> <br /> public int getReplacedFromContractTariffId() {<br /> return replacedFromContractTariffId;<br /> }<br /> <br /> public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {<br /> this.replacedFromContractTariffId = replacedFromContractTariffId;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом<br /> *<br /> */<br /> @WebService<br /> @XmlSeeAlso({ContractTariff.class})<br /> public abstract interface ContractTariffService {<br /> @WebMethod<br /> public abstract List&lt;ContractTariff&gt; contractTariffList(@WebParam(name=&quot;contractId&quot;) int contracId) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.util.List;<br /> <br /> /**<br /> * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService&quot;)<br /> public class ContractTariffServiceImpl extends AbstractService<br /> implements ContractTariffService {<br /> <br /> @Override<br /> public List&lt;ContractTariff&gt; contractTariffList(@WebParam(name = &quot;contractId&quot;) int contracId) throws BGException {<br /> return (new ContractTariffDao(getConnection()).list(contracId));<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Веб-сервис работы с переопределёнными ценами:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Интерфейс веб-сервиса для работы с персональными ценами<br /> */<br /> @WebService<br /> @XmlSeeAlso({CustomTariffCost.class})<br /> public abstract interface CustomTariffCostService {<br /> <br /> @WebMethod<br /> public abstract List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id)<br /> throws BGException;<br /> <br /> @WebMethod<br /> public abstract void updateCustomTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs)<br /> throws BGException;<br /> <br /> /**<br /> * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> */<br /> @WebMethod<br /> public abstract Set&lt;Integer&gt; availableServiceList(@WebParam(name=&quot;tariffPlanId&quot;) int tpid) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;<br /> import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;<br /> import ru.bitel.bgbilling.server.util.ClosedDateChecker;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.List;<br /> import java.util.Set;<br /> import java.util.TreeSet;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * Реализация веб-сервиса для работы с персональными ценам<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService&quot;)<br /> public class CustomTariffCostServiceImpl extends AbstractService<br /> implements CustomTariffCostService {<br /> <br /> @Override<br /> public List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name = &quot;contractTariffId&quot;) int contract_tariff_id) throws BGException {<br /> return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);<br /> }<br /> <br /> @Override<br /> public void updateCustomTariffCostList(@WebParam(name = &quot;contractTariffId&quot;)int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs) throws BGException{<br /> <br /> Calendar closedDate;<br /> ContractTariffManager ctm = new ContractTariffManager(getConnection());<br /> ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);<br /> if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking(&quot;updateCustomTariffCostList&quot;, 0, this.userId)) != null)<br /> {<br /> ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());<br /> }<br /> CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());<br /> try {<br /> ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);<br /> } catch (SQLException e) {<br /> throw new BGException(e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> * На данный момент реализовано через метки - ищем все метки тарифного плана вида &quot;id - Название&quot;,<br /> * где id - это id услуги. Например &quot;5 - Интернет&quot;.<br /> */<br /> @Override<br /> public Set&lt;Integer&gt; availableServiceList(@WebParam(name = &quot;tariffPlanId&quot;) int tpid)<br /> throws BGException<br /> {<br /> Set&lt;Integer&gt; result = new TreeSet&lt;Integer&gt;();<br /> TariffLabelManager tlm = new TariffLabelManager(getConnection());<br /> //Получаем список id меток тарифа<br /> Set&lt;Integer&gt; tariffLabelIds = tlm.getTariffLabelIds(tpid);<br /> if(tariffLabelIds.size()&gt;0){<br /> //Берём список всех меток с названиями<br /> List&lt;TariffLabelItem&gt; tariffLabelItemList = tlm.getTariffLabelItemList();<br /> //Ищем среди меток тарифа метки с названиями вида &quot;id - Название&quot;<br /> /*for(Integer label_id : tariffLabelIds){<br /> TariffLabelItem label = tariffLabelItemList.get()<br /> }*/<br /> Pattern pattern = Pattern.compile(&quot;^(\\d+) - .+$&quot;);<br /> <br /> Matcher matcher;<br /> //Перебираем все метки из справочника<br /> for(TariffLabelItem label : tariffLabelItemList){<br /> //Если метка есть на нашем тарифе ...<br /> if(tariffLabelIds.contains(label.getId())){<br /> //...то смотрим её название<br /> matcher = pattern.matcher(label.getTitle());<br /> if(matcher.find()){//определился sid услуги - отлично, добавляем в результат<br /> try{<br /> result.add(Integer.valueOf(matcher.group(1)));<br /> }catch (Exception e){}<br /> }<br /> }<br /> }<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> ==== Клиетский интерфейс ====<br /> <br /> Для начала немного костылей.<br /> <br /> Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:<br /> <br /> [[Файл:custom_cost_labels.jpg]]<br /> <br /> Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида &quot;xxx - Наименование&quot;, где xxx - sid услуги, дополненный нулями до 3 символов, и проставить эту метку для всех необходимых тарифов.<br /> <br /> Переходим собственно к интерфейсу.<br /> <br /> Менять клиентский интерфейс биллинга стандартным образом нельзя - только через переопределение jar-файлов клиента.<br /> <br /> Я долго думал, куда бы запихать управление персональными ценами малой кровью.<br /> По-идее, будь я разработчиком Битела, я бы сделал это в ядре (т.к. в общем случае можно реализовать переопределение не только абонплат). Соответственно, редактирование персональных цен должно быть где-то в разделе &quot;Тарифные планы&quot; договора - например, отдельной вкладкой после &quot;Персональных тарифов&quot; и &quot;Тарифных опций&quot;.<br /> Но малой кровью модифицировать эту клиентскую панель не получилось, поэтому было решено сделать отдельную вкладку &quot;Переопределение цен&quot; на странице модуля Npay - см http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost.jpg<br /> <br /> <br /> Привожу клиентский код для 5.2<br /> Что с ним делать - решайте сами на свой страх и риск :)<br /> <br /> Добавляем нашу панель - прописываем в клиентский файл bitel/billing/module/services/npay/setup_user.properties:<br /> <br /> &lt;source lang=ini&gt;<br /> module.tab.3.class=bitel.billing.module.services.npay.CustomCostModulePanel<br /> module.tab.3.title=\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0446\u0435\u043D<br /> &lt;/source&gt;<br /> <br /> Панель:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.module.common.BGControlPanelContractSelect;<br /> import bitel.billing.module.common.BGTitleBorder;<br /> import bitel.billing.module.common.DialogToolBar;<br /> import bitel.billing.module.common.FloatTextField;<br /> import ru.bitel.bgbilling.client.common.BGEditor;<br /> import ru.bitel.bgbilling.client.common.BGUPanel;<br /> import ru.bitel.bgbilling.client.common.BGUTable;<br /> import ru.bitel.bgbilling.client.common.ClientContext;<br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.Service;<br /> import ru.bitel.bgbilling.kernel.module.common.service.ServiceService;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.client.AbstractBGUPanel;<br /> import ru.bitel.common.client.BGSwingUtilites;<br /> import ru.bitel.common.client.BGUComboBox;<br /> import ru.bitel.common.client.table.BGTableModel;<br /> import ru.bitel.common.model.KeyValue;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.swing.*;<br /> import java.awt.*;<br /> import java.awt.event.ActionEvent;<br /> import java.awt.event.ActionListener;<br /> import java.math.BigDecimal;<br /> import java.util.*;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Панелька на клиенте для редактирования переопределений стоимости тарифных планов договоров.<br /> */<br /> public class CustomCostModulePanel extends BGUPanel<br /> {<br /> private BGControlPanelContractSelect contractSelect;<br /> private ContractTariffService wsContractTariff = getContext().getPort(ContractTariffService.class, 0);<br /> <br /> private BGTableModel&lt;ContractTariff&gt; model = new BGTableModel&lt;ContractTariff&gt;(&quot;contractTariff&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, Integer.class, 0, 0, 0, &quot;id&quot;, true, false);<br /> addColumn(&quot;TPID&quot;, Integer.class, 0, 0, 0, &quot;tpid&quot;, true, false);<br /> addColumn(&quot;Название&quot;, &quot;tariffName&quot;, true);<br /> addColumn(&quot;Период&quot;, &quot;period&quot;, true);<br /> addColumn(&quot;Комментарий&quot;, &quot;comment&quot;, false);<br /> addColumn(&quot;Поз.&quot;, &quot;pos&quot;, true);<br /> }<br /> <br /> @Override<br /> public Object getValue(ContractTariff val, int column)<br /> {<br /> Object result = &quot;&quot;;<br /> switch (column)<br /> {<br /> case 3:<br /> result = TimeUtils.formatPeriod(val.getDate1(), val.getDate2());<br /> break;<br /> default:<br /> result = super.getValue(val, column);<br /> }<br /> return result;<br /> }<br /> };<br /> <br /> public CustomCostModulePanel()<br /> {<br /> super(new BorderLayout());<br /> //setLayout(new GridBagLayout());<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;refresh&quot;, &quot;Обновить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostModulePanel.this.setData();<br /> }<br /> };<br /> }<br /> <br /> public void setData() throws BGException {<br /> String cids = CustomCostModulePanel.this.contractSelect.getContracts();<br /> int cid = -1;<br /> try{<br /> cid = Utils.toIntegerList(cids).get(0);<br /> }catch(Exception ex){<br /> this.processException(ex);<br /> }<br /> <br /> CustomCostModulePanel.this.model.setData(CustomCostModulePanel.this.wsContractTariff.contractTariffList(cid));<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> setLayout(new GridBagLayout());<br /> BGUTable table = new BGUTable(this.model);<br /> <br /> JButton okButton = new JButton(&quot;OK&quot;);<br /> okButton.setMargin(new Insets(2, 2, 2, 2));<br /> okButton.addActionListener(new ActionListener() {<br /> public void actionPerformed(ActionEvent e) {<br /> try {<br /> CustomCostModulePanel.this.setData();<br /> } catch (BGException e1) {<br /> processException(e1);<br /> }<br /> }<br /> });<br /> this.contractSelect = new BGControlPanelContractSelect(true, true);<br /> this.contractSelect.add(okButton, new GridBagConstraints(2, 0, 1, 1, 0.0D, 0.0D, 10, 0,<br /> new Insets(0, 0, 5, 5), 0, 0));<br /> <br /> add(BGSwingUtilites.wrapBorder(this.contractSelect, &quot;Выберите договор&quot;), new GridBagConstraints(0, 0, 1, 1, 1.0D, 0.0D, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.setVisible(false);<br /> editor.addForm(new CustomCostTariffPanel(getContext()));<br /> add(BGSwingUtilites.wrapBorder(new JScrollPane(table), &quot;Тарифные планы договора&quot;),<br /> new GridBagConstraints(0, 1, 1, 1, 1.0D, 0.6D, 10, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 1.0D, GridBagConstraints.SOUTH, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> BGSwingUtilites.handleEdit(table, editor);<br /> }<br /> <br /> /**<br /> * Панель редактирования собственно списка переопределений услуг конкретного тарифа договора<br /> */<br /> public class CustomCostTariffPanel extends BGUPanel {<br /> private BGUTable table;<br /> CustomTariffCostService wsCustomCost = this.getContext().getPort(CustomTariffCostService.class, 0);<br /> private Map&lt;Integer, Service&gt; servicesMap;//Справочник услуг всех модулей : sid -&gt; Service<br /> private ContractTariff current;<br /> <br /> private BGTableModel&lt;CustomCostRow&gt; model = new BGTableModel&lt;CustomCostRow&gt;(&quot;customCosts&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, 0, 0, 0, null, true);<br /> addColumn(&quot;Услуга&quot;, -1, 120, -1, null, false);<br /> addColumn(&quot;Цена&quot;, BigDecimal.class, -1, 180, -1, null, false);<br /> }<br /> <br /> public Object getValue(CustomCostRow val, int column)<br /> {<br /> switch (column)<br /> {<br /> case 0:<br /> return val.sid;<br /> case 1:<br /> return val.title;<br /> case 2:<br /> return val.value;<br /> default:<br /> return super.getValue(val, column);<br /> }<br /> }<br /> };<br /> <br /> protected void initActions(){<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if ((CustomCostTariffPanel.this.current = CustomCostModulePanel.this.model.getSelectedRow()) != null)<br /> {<br /> CustomCostTariffPanel.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;Сохранить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if(CustomCostTariffPanel.this.current!=null){<br /> //CustomCostTariffPanel.this.model.getRows();<br /> List&lt;CustomTariffCost&gt; costs = new ArrayList&lt;CustomTariffCost&gt;();<br /> if(CustomCostTariffPanel.this.model.getRows()!=null){<br /> CustomTariffCost cost;<br /> for(CustomCostRow row : CustomCostTariffPanel.this.model.getRows()){<br /> cost = new CustomTariffCost(CustomCostTariffPanel.this.current.getId(),row.sid,row.value);<br /> costs.add(cost);<br /> }<br /> }<br /> CustomCostTariffPanel.this.wsCustomCost.updateCustomTariffCostList(<br /> CustomCostTariffPanel.this.current.getId(),<br /> costs<br /> );<br /> }<br /> CustomCostTariffPanel.this.performActionClose();<br /> }<br /> };<br /> }<br /> <br /> @Override<br /> public void performActionOpen()<br /> {<br /> //расставляем данные в форме редактирования, затем отображаем её<br /> setData(this.current.getId(), this.current.getTariffPlanId());<br /> super.performActionOpen();<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> //To change body of implemented methods use File | Settings | File Templates.<br /> setLayout(new GridBagLayout());<br /> this.table = new BGUTable(this.model);<br /> DialogToolBar toolBar = new DialogToolBar();<br /> <br /> ParamForm paramForm = new ParamForm(getContext());<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.addForm(paramForm);<br /> editor.setVisible(false);<br /> <br /> BGSwingUtilites.buildToolBar(toolBar, paramForm);<br /> toolBar.compact();<br /> <br /> BGSwingUtilites.handleEdit(this.table, editor);<br /> <br /> add(toolBar, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 0), 0, 0));<br /> add(new JScrollPane(this.table), new GridBagConstraints(0, 1, 1, 1, 1.0D, 1.0D, 10, 1, new Insets(0, 0,<br /> 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(5, 5, 5, 5), 0, 0));<br /> }<br /> <br /> //Сравниваем строчки цен для сортировки в таблице<br /> private final Comparator&lt;CustomCostRow&gt; COMPARATOR = new Comparator&lt;CustomCostRow&gt;()<br /> {<br /> public int compare(CustomCostRow o1, CustomCostRow o2)<br /> {<br /> int result = Integer.valueOf(o1.sid).compareTo(o2.sid);<br /> if (result != 0)<br /> {<br /> return result;<br /> }<br /> <br /> return o1.title.compareTo(o2.title);<br /> }<br /> };<br /> <br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> private BGUComboBox&lt;KeyValue&lt;Integer, String&gt;&gt; keyCombo = new BGUComboBox(<br /> KeyValue.class);<br /> <br /> public CustomCostTariffPanel(ClientContext context)<br /> {<br /> super(context);<br /> //Кэшируем список всех услуг биллинга. Если возникнет ошибка при его получении - ругаемся<br /> try {<br /> java.util.List&lt;Service&gt; serviceList = CustomCostTariffPanel.this.getContext().getPort(ServiceService.class, 0).serviceList(0);<br /> this.servicesMap = new HashMap&lt;Integer, Service&gt;(serviceList.size());<br /> for(Service s : serviceList){<br /> this.servicesMap.put(s.getId(),s);<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> }<br /> }<br /> <br /> public void setData(int id, int tpid){<br /> java.util.List&lt;CustomTariffCost&gt; costList;<br /> try {<br /> costList = wsCustomCost.customTariffCostList(id);<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> java.util.List&lt;CustomCostRow&gt; data = new ArrayList&lt;CustomCostRow&gt;(costList.size());<br /> String title;<br /> Service s;<br /> for(CustomTariffCost cost : costList){<br /> s = this.servicesMap.get(cost.getSid());<br /> if(s!=null){<br /> title = s.getTitle();<br /> }else{<br /> title = String.valueOf(cost.getSid());<br /> }<br /> data.add(new CustomCostRow(cost.getSid(), title, cost.getValue()));<br /> }<br /> Collections.sort(data, COMPARATOR);<br /> this.model.setData(data);<br /> <br /> //Получаем для комбобокса список услуг, для которых разрешено переопределение на этом тарифе<br /> java.util.List&lt;KeyValue&lt;Integer, String&gt;&gt; typeList = new ArrayList&lt;KeyValue&lt;Integer, String&gt;&gt;(CustomCostTariffPanel.this.servicesMap.size());<br /> try{<br /> for(Integer sid : this.wsCustomCost.availableServiceList(tpid)){<br /> s = this.servicesMap.get(sid);<br /> if(null!=s){<br /> title = s.getTitle();<br /> }else{<br /> title = &quot;&lt;&quot;+sid+&quot;&gt;&quot;;<br /> }<br /> <br /> typeList.add(new KeyValue&lt;Integer, String&gt;(sid, title));<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> CustomCostTariffPanel.this.keyCombo.setData(typeList);<br /> }<br /> <br /> class CustomCostRow<br /> {<br /> public int sid;<br /> public String title;<br /> public BigDecimal value;<br /> <br /> public CustomCostRow(int sid, String title, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.title = title;<br /> this.value = value;<br /> }<br /> <br /> /*public CustomCostRow(int sid, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.value = value;<br /> }*/<br /> }<br /> <br /> class ParamForm extends BGUPanel<br /> {<br /> private FloatTextField value = new FloatTextField();<br /> private CustomCostRow current;<br /> private boolean edit;<br /> <br /> public ParamForm(ClientContext context)<br /> {<br /> super(context);<br /> }<br /> <br /> protected void jbInit()<br /> {<br /> setBorder(BorderFactory.createCompoundBorder(new BGTitleBorder(&quot; Редактор &quot;), BorderFactory.createEmptyBorder(0, 3, 3, 3)));<br /> <br /> add(CustomCostTariffPanel.this.keyCombo, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 5), 0, 0));<br /> <br /> add(this.value, new GridBagConstraints(1, 0, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(0, 0, 0, 0),<br /> 0, 0));<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;new&quot;, &quot;Добавить&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = false;<br /> CustomCostTariffPanel.ParamForm.this.current = new CustomCostRow(0,&quot;&quot;,BigDecimal.ZERO);<br /> <br /> CustomCostTariffPanel.ParamForm.this.value.setText(&quot;&quot;);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(true);<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = true;<br /> <br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.keyCombo.setSelectedItem(ParamForm.this.current.sid);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(false);<br /> <br /> ParamForm.this.value.setText(String.valueOf(ParamForm.this.current.value));<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;delete&quot;, &quot;Удалить&quot;, ClientUtils.getIcon(&quot;item_delete&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> <br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.model.deleteSelectedRow();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;OK&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> KeyValue&lt;Integer, String&gt; key = CustomCostTariffPanel.this.keyCombo.getSelectedItem();<br /> if (key == null)<br /> {<br /> return;<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.current.sid = key.getKey();<br /> CustomCostTariffPanel.ParamForm.this.current.title = key.getValue();<br /> CustomCostTariffPanel.ParamForm.this.current.value = Utils.parseBigDecimal(CustomCostTariffPanel.ParamForm.this.value.getText(), BigDecimal.ZERO);<br /> <br /> if (!CustomCostTariffPanel.ParamForm.this.edit)<br /> {<br /> CustomCostTariffPanel.this.model.addRow(CustomCostTariffPanel.ParamForm.this.current);<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionClose();<br /> }<br /> };<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> Клиентские узлы тарифа:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел DayModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;за день&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;за месяц&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;за день&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;за месяц&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел MonthModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> <br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;безусловно&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;пропорц. периоду&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;2&quot;, &quot;пропорц. объему&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;3&quot;, &quot;как выгоднее&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;безусловно&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;пропорц. периоду&quot;);<br /> }<br /> else if (this.type.equals(&quot;2&quot;))<br /> {<br /> title.append(&quot;пропорц. объему&quot;);<br /> }<br /> else if (this.type.equals(&quot;3&quot;))<br /> {<br /> title.append(&quot;как выгоднее&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел YearModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Стоимость: &quot;));<br /> edit.add(this.costTf);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(&quot;Стоимость: &quot;);<br /> title.append(this.cost);<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.custom = Utils.parseBoolean((String) data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> this.costTf.setText(this.cost);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> === Отображение на договоре ===<br /> <br /> На договоре персональные цены на услуги будем писать другим шрифтом в скобках через запятую после наименования тарифа в дереве.<br /> <br /> Для этого переопределим через dynaction стандартный action вывода карточки договора:<br /> <br /> &lt;source lang=ini&gt;<br /> dynaction:contract.hierarchy.ActionContractInfo=ru.dsi.bgbilling.kernel.scripts.action.ActionContractInfo<br /> &lt;/source&gt;<br /> <br /> Переопределённый action делает много всяких штук, кроме подсветки персональных цен. Привожу целиком, лишнее можно убрать по необходимости:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.scripts.action.contract;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.ApplicationModule;<br /> import bitel.billing.server.call.bean.Login;<br /> import bitel.billing.server.contract.bean.ContractModuleManager;<br /> import bitel.billing.server.contract.bean.PersonalTariff;<br /> import bitel.billing.server.contract.bean.PersonalTariffManager;<br /> import bitel.billing.server.dialup.bean.DialUpLoginManager;<br /> import bitel.billing.server.email.bean.Account;<br /> import bitel.billing.server.email.bean.AccountManager;<br /> import bitel.billing.server.npay.bean.ServiceObject;<br /> import bitel.billing.server.npay.bean.ServiceObjectManager;<br /> import bitel.billing.server.phone.bean.ClientItem;<br /> import bitel.billing.server.phone.bean.ClientItemManager;<br /> import bitel.billing.server.voiceip.bean.VoiceIpLoginManager;<br /> import org.w3c.dom.Element;<br /> import org.w3c.dom.NodeList;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.base.server.DefaultContext;<br /> import ru.bitel.bgbilling.kernel.container.security.server.ModuleAction;<br /> import ru.bitel.bgbilling.kernel.container.security.server.PermissionChecker;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalance;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalanceManager;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.BGModule;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.api.server.bean.InetServDao;<br /> import ru.bitel.bgbilling.modules.ipn.common.bean.AddressRange;<br /> import ru.bitel.bgbilling.modules.ipn.server.bean.AddressRangeManager;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.XMLUtils;<br /> import ru.bitel.common.worker.ThreadContext;<br /> <br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.Date;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class ActionContractInfo extends bitel.billing.server.contract.action.ActionContractInfo{<br /> @Override<br /> public void doAction() throws SQLException, BGException<br /> {<br /> super.doAction();<br /> <br /> Date now = new Date();<br /> <br /> //Удаляем модули<br /> Element modulesNode = XMLUtils.selectElement(rootNode, &quot;/data/info/modules&quot;);<br /> if(modulesNode!=null) {<br /> NodeList list = modulesNode.getChildNodes();<br /> while (modulesNode.hasChildNodes()){<br /> modulesNode.removeChild(list.item(0));<br /> }<br /> }<br /> //Количество сущностей модуля<br /> int itemCount;<br /> //Количество открытых датой сущностей модуля<br /> int openItemCount;<br /> InetServDao servDao;<br /> InetServ tree;<br /> ClientItemManager clientItemManager;<br /> DialUpLoginManager lm;<br /> ServiceObjectManager manager;<br /> AccountManager accountManager;<br /> AddressRangeManager arm;<br /> <br /> //Добавляем модули заново с доп данными:<br /> for (BGModule module : new ContractModuleManager(this.con).getContractModules(cid))<br /> {<br /> Element item = createElement(modulesNode, &quot;item&quot;);<br /> String className = module.getInstalledModule().getPackageServer() + &quot;.Module&quot;;<br /> ApplicationModule moduleClass = (ApplicationModule) Utils.newInstance(className, ApplicationModule.class);<br /> itemCount=-1;<br /> openItemCount=-1;<br /> //Inet<br /> if(moduleClass instanceof ru.bitel.bgbilling.modules.inet.api.server.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> servDao = new InetServDao(con, module.getId(), 0);<br /> tree = servDao.tree(this.cid);<br /> if(tree.getChildren()!=null){<br /> itemCount = tree.getChildren().size();<br /> for (InetServ inetServ : tree.getChildren()) {<br /> if(TimeUtils.dateBefore(now, inetServ.getDateTo()) || inetServ.getDateTo()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> servDao.recycle();<br /> }<br /> <br /> //Phone<br /> if(moduleClass instanceof bitel.billing.server.phone.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> clientItemManager = new ClientItemManager(con, module.getId());<br /> for (ClientItem clientItem : clientItemManager.getItemList(this.cid)) {<br /> itemCount++;<br /> if(TimeUtils.dateBefore(Calendar.getInstance(), clientItem.getDate2()) || clientItem.getDate2()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Dialup<br /> if(moduleClass instanceof bitel.billing.server.dialup.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> lm = new DialUpLoginManager(con, module.getId());<br /> for (Login login : lm.getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Npay<br /> if(moduleClass instanceof bitel.billing.server.npay.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> manager = new ServiceObjectManager(con, module.getId());<br /> for (ServiceObject so : manager.getServiceObjectList(this.cid, null)) {<br /> itemCount++;<br /> if(so.getDate2()==null || TimeUtils.dateBefore(now, so.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Email<br /> if(moduleClass instanceof bitel.billing.server.email.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> accountManager = new AccountManager(con, module.getId());<br /> for (Account acc : accountManager.getContractAccountList(this.cid)) {<br /> itemCount++;<br /> if(acc.getDate2()==null || TimeUtils.dateBefore(now, acc.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //IPN<br /> if(moduleClass instanceof bitel.billing.server.ipn.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> arm = new AddressRangeManager(con, module.getId());<br /> for (AddressRange range: arm.getContractRanges(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> for (AddressRange range: arm.getContractNets(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //VoiceIP<br /> if(moduleClass instanceof bitel.billing.server.voiceip.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> for (Login login : new VoiceIpLoginManager(con, module.getId()).getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> <br /> item.setAttribute(&quot;id&quot;, String.valueOf(module.getId()));<br /> if(itemCount&gt;=0 &amp;&amp; openItemCount&gt;=0){<br /> item.setAttribute(&quot;title&quot;, &quot;&lt;html&gt;&quot;+module.getTitle()+&quot; &lt;font style='color: grey;' size='2'&gt;[&quot;+openItemCount+&quot;/&quot;+itemCount+&quot;]&lt;/font&gt;&lt;/htmp&gt;&quot;);<br /> }else{<br /> item.setAttribute(&quot;title&quot;, module.getTitle());<br /> }<br /> item.setAttribute(&quot;package&quot;, module.getInstalledModule().getPackageClient());<br /> if (moduleClass != null)<br /> {<br /> item.setAttribute(&quot;status&quot;, moduleClass.getStatus(this.con, module.getId(), cid));<br /> }<br /> }<br /> <br /> Element tariff = XMLUtils.selectElement(rootNode, &quot;/data/info/tariff&quot;);<br /> //Удаляем все тарифы из списка - будем строить свой список<br /> if(tariff!=null){<br /> NodeList list = tariff.getChildNodes();<br /> while(tariff.hasChildNodes()){<br /> tariff.removeChild(list.item(0));<br /> }<br /> /*for(int i = 0;i&lt;list.getLength();i++){<br /> tariff.removeChild(list.item(i));<br /> }*/<br /> //Составляем свой список тарифов с красивостями<br /> Map&lt;String, String&gt; mapRequest = new HashMap&lt;String, String&gt;();<br /> mapRequest.put(&quot;action&quot;, &quot;PersonalTariffTable&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract.tariff&quot;);<br /> <br /> ModuleAction action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> for (PersonalTariff personalTariff : new PersonalTariffManager(this.con)<br /> .getPersonalTariffList(cid, new Date())) {<br /> addListItem(tariff, personalTariff.getId(), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;&quot; + &quot;ПТ: &quot; + personalTariff.getTitle()+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> mapRequest.clear();<br /> mapRequest.put(&quot;action&quot;, &quot;ContractTariffPlans&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract&quot;);<br /> <br /> action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> String query = &quot;SELECT t2.id, t2.title, group_concat(TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM ctc.value))) value, t1.date1 FROM contract_tariff AS t1 left join tariff_plan AS t2 on t1.tpid=t2.id left join custom_tariff_cost ctc on t1.id=ctc.contract_tariff_id &quot; +<br /> &quot; WHERE t1.cid=? AND emid=0 AND eid=0 AND ( isNull( date2 ) OR date2&gt;=CURDATE() ) group by t1.id&quot;;<br /> //AND (isNull( date1 ) OR date1&lt;=CURDATE())<br /> PreparedStatement ps = this.con.prepareStatement(query);<br /> ps.setInt(1, cid);<br /> ResultSet rs = ps.executeQuery();<br /> String value;<br /> Date dt1;<br /> String name;<br /> while (rs.next())<br /> {<br /> value = rs.getString(3);<br /> name = rs.getString(2);<br /> //21.12.2016 by semyon@dsi.ru - show future tariffs too, but with another color<br /> dt1=TimeUtils.convertSqlDateToDate(rs.getDate(4));<br /> if(TimeUtils.dateBefore(now,dt1)){<br /> name = &quot;&lt;font style='color: #cc99ff;'&gt;&quot;+name+&quot;&lt;/font&gt;&quot;;<br /> }<br /> if(value==null){<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&quot;+name+&quot;&lt;/html&gt;&quot;);<br /> }else{<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;*&lt;/font&gt; &quot;+name+&quot; &lt;font style='color: green;'&gt;(&quot;+value+&quot;)&lt;/font&gt;&lt;/html&gt;&quot; );<br /> }<br /> }<br /> rs.close();<br /> ps.close();<br /> }<br /> }<br /> <br /> //Подсвечиваем статус &quot;закрыт&quot;<br /> Element contractElement = XMLUtils.selectElement(rootNode, &quot;contract&quot;);<br /> if(contractElement!=null){<br /> if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;закрыт&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;закрыт&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;приостановлен&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: blue;'&gt;приостановлен&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;в отключении&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: #5F84B6;'&gt;в отключении&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> //Проверяем баланс и лимит на договоре - выделяем лимит красным, если баланс&lt;лимита<br /> if(contractElement!=null){<br /> ConvergenceBalanceManager convergenceBalanceManager = ConvergenceBalanceManager.getInstance();<br /> if (convergenceBalanceManager != null) {<br /> ConvergenceBalance balance = convergenceBalanceManager.getBalance((ThreadContext.get(DefaultContext.class)).getConnectionSet(), this.cid, System.currentTimeMillis());<br /> if(balance!=null &amp;&amp; !balance.isBalanceUnderLimit()){<br /> contractElement.setAttribute(&quot;limit&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;&quot;+contractElement.getAttribute(&quot;limit&quot;)+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> }<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> == Заключение ==<br /> <br /> У читателя, вероятно, возникает резонный вопрос: &quot;Где howto? Дай мне готовый файлик, чтобы пользоваться фичей!&quot;.<br /> Но пока &lt;s&gt;мне лень&lt;/s&gt; я не буду выкладывать готовых компилированных файлов и инструкций по подмене jar клиента и сервера.<br /> <br /> * Во-первых, это всё работает на 5.2 и на старших версиях не тестировалось - наверняка вам всё равно придётся многое переписывать.<br /> * Во-вторых, использовать подмену стандартных классов плохо для кармы - не хочу, чтобы этим массово пользовались<br /> * В-третьих, давайте сперва обсудим фичу на [https://forum.bitel.ru/viewtopic.php?f=66&amp;t=9709 форуме]<br /> <br /> Хотелось бы, чтобы что-то подобное появилось в стандартной реализации от Битела. Собственно, в первую очередь поэтому я и решил выложить решение спустя 4 года после внедрения :) <br /> <br /> Сейчас у меня в табличке custom_tariff_cost уже 5700 записей, фича работает как часы. Но есть минус - из-за большого количества таких фич в продакшене осложняется обновление на версии 6 и 7: нужно предварительно тестировать и переписывать большой объём кода.<br /> <br /> Если вам интересны персональные тарифы на договоре, &lt;s&gt;подписывайтесь, ставьте лайк&lt;/s&gt; пишите на форум разработчикам - чем больше будет запросов, тем быстрее появится стандартная реализация.<br /> <br /> Также, если будет интерес, могу выложить свою реализацию процентных скидок на услуги. Она менее &quot;костыльная&quot; - не требуется подмена классов, только npay.xml для собственных тарифных узлов.<br /> <br /> ps. Ещё можно скинуться автору на [https://money.yandex.ru/to/41001959637999 кофе] :)<br /> --[[Участник:Cromeshnic|Cromeshnic]] 08:00, 8 мая 2017 (UTC)</div> Mon, 08 May 2017 08:00:54 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Отображение на договоре */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> <br /> Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.<br /> <br /> ==== Веб-сервис переопределения цен ====<br /> <br /> Вспомогательные классы:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import ru.bitel.common.model.Id;<br /> <br /> import java.io.Serializable;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Тарифный план договора для использования в веб-сервисе.<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя<br /> *<br /> */<br /> public class ContractTariff extends Id implements Serializable {<br /> private int contractId;<br /> private int tariffPlanId;<br /> private Date date1;<br /> private Date date2;<br /> private String comment = &quot;&quot;;<br /> private String tariffName;<br /> private int pos;<br /> private int entityMid;<br /> private int entityId;<br /> private int replacedFromContractTariffId;<br /> <br /> public ContractTariff() {<br /> this.id = -1;<br /> this.contractId = -1;<br /> this.tariffPlanId = -1;<br /> this.date1 = null;<br /> this.date2 = null;<br /> this.tariffName = null;<br /> this.pos = 0;<br /> this.entityId = 0;<br /> this.entityMid = 0;<br /> this.replacedFromContractTariffId = -1;<br /> }<br /> <br /> public int getContractId() {<br /> return contractId;<br /> }<br /> <br /> public void setContractId(int contractId) {<br /> this.contractId = contractId;<br /> }<br /> <br /> public int getTariffPlanId() {<br /> return tariffPlanId;<br /> }<br /> <br /> public String getTariffName() {<br /> return tariffName;<br /> }<br /> <br /> public void setTariffName(String tariffName) {<br /> this.tariffName = tariffName;<br /> }<br /> <br /> public void setTariffPlanId(int tariffPlanId) {<br /> this.tariffPlanId = tariffPlanId;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> <br /> public String getComment() {<br /> return comment;<br /> }<br /> <br /> public void setComment(String comment) {<br /> this.comment = comment;<br /> }<br /> <br /> public int getPos() {<br /> return pos;<br /> }<br /> <br /> public void setPos(int pos) {<br /> this.pos = pos;<br /> }<br /> <br /> public int getEntityMid() {<br /> return entityMid;<br /> }<br /> <br /> public void setEntityMid(int entityMid) {<br /> this.entityMid = entityMid;<br /> }<br /> <br /> public int getEntityId() {<br /> return entityId;<br /> }<br /> <br /> public void setEntityId(int entityId) {<br /> this.entityId = entityId;<br /> }<br /> <br /> public int getReplacedFromContractTariffId() {<br /> return replacedFromContractTariffId;<br /> }<br /> <br /> public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {<br /> this.replacedFromContractTariffId = replacedFromContractTariffId;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом<br /> *<br /> */<br /> @WebService<br /> @XmlSeeAlso({ContractTariff.class})<br /> public abstract interface ContractTariffService {<br /> @WebMethod<br /> public abstract List&lt;ContractTariff&gt; contractTariffList(@WebParam(name=&quot;contractId&quot;) int contracId) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.util.List;<br /> <br /> /**<br /> * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService&quot;)<br /> public class ContractTariffServiceImpl extends AbstractService<br /> implements ContractTariffService {<br /> <br /> @Override<br /> public List&lt;ContractTariff&gt; contractTariffList(@WebParam(name = &quot;contractId&quot;) int contracId) throws BGException {<br /> return (new ContractTariffDao(getConnection()).list(contracId));<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Веб-сервис работы с переопределёнными ценами:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Интерфейс веб-сервиса для работы с персональными ценами<br /> */<br /> @WebService<br /> @XmlSeeAlso({CustomTariffCost.class})<br /> public abstract interface CustomTariffCostService {<br /> <br /> @WebMethod<br /> public abstract List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id)<br /> throws BGException;<br /> <br /> @WebMethod<br /> public abstract void updateCustomTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs)<br /> throws BGException;<br /> <br /> /**<br /> * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> */<br /> @WebMethod<br /> public abstract Set&lt;Integer&gt; availableServiceList(@WebParam(name=&quot;tariffPlanId&quot;) int tpid) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;<br /> import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;<br /> import ru.bitel.bgbilling.server.util.ClosedDateChecker;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.List;<br /> import java.util.Set;<br /> import java.util.TreeSet;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * Реализация веб-сервиса для работы с персональными ценам<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService&quot;)<br /> public class CustomTariffCostServiceImpl extends AbstractService<br /> implements CustomTariffCostService {<br /> <br /> @Override<br /> public List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name = &quot;contractTariffId&quot;) int contract_tariff_id) throws BGException {<br /> return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);<br /> }<br /> <br /> @Override<br /> public void updateCustomTariffCostList(@WebParam(name = &quot;contractTariffId&quot;)int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs) throws BGException{<br /> <br /> Calendar closedDate;<br /> ContractTariffManager ctm = new ContractTariffManager(getConnection());<br /> ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);<br /> if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking(&quot;updateCustomTariffCostList&quot;, 0, this.userId)) != null)<br /> {<br /> ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());<br /> }<br /> CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());<br /> try {<br /> ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);<br /> } catch (SQLException e) {<br /> throw new BGException(e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> * На данный момент реализовано через метки - ищем все метки тарифного плана вида &quot;id - Название&quot;,<br /> * где id - это id услуги. Например &quot;5 - Интернет&quot;.<br /> */<br /> @Override<br /> public Set&lt;Integer&gt; availableServiceList(@WebParam(name = &quot;tariffPlanId&quot;) int tpid)<br /> throws BGException<br /> {<br /> Set&lt;Integer&gt; result = new TreeSet&lt;Integer&gt;();<br /> TariffLabelManager tlm = new TariffLabelManager(getConnection());<br /> //Получаем список id меток тарифа<br /> Set&lt;Integer&gt; tariffLabelIds = tlm.getTariffLabelIds(tpid);<br /> if(tariffLabelIds.size()&gt;0){<br /> //Берём список всех меток с названиями<br /> List&lt;TariffLabelItem&gt; tariffLabelItemList = tlm.getTariffLabelItemList();<br /> //Ищем среди меток тарифа метки с названиями вида &quot;id - Название&quot;<br /> /*for(Integer label_id : tariffLabelIds){<br /> TariffLabelItem label = tariffLabelItemList.get()<br /> }*/<br /> Pattern pattern = Pattern.compile(&quot;^(\\d+) - .+$&quot;);<br /> <br /> Matcher matcher;<br /> //Перебираем все метки из справочника<br /> for(TariffLabelItem label : tariffLabelItemList){<br /> //Если метка есть на нашем тарифе ...<br /> if(tariffLabelIds.contains(label.getId())){<br /> //...то смотрим её название<br /> matcher = pattern.matcher(label.getTitle());<br /> if(matcher.find()){//определился sid услуги - отлично, добавляем в результат<br /> try{<br /> result.add(Integer.valueOf(matcher.group(1)));<br /> }catch (Exception e){}<br /> }<br /> }<br /> }<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> ==== Клиетский интерфейс ====<br /> <br /> Для начала немного костылей.<br /> <br /> Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:<br /> <br /> [[Файл:custom_cost_labels.jpg]]<br /> <br /> Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида &quot;xxx - Наименование&quot;, где xxx - sid услуги, дополненный нулями до 3 символов, и проставить эту метку для всех необходимых тарифов.<br /> <br /> Переходим собственно к интерфейсу.<br /> <br /> Менять клиентский интерфейс биллинга стандартным образом нельзя - только через переопределение jar-файлов клиента.<br /> <br /> Я долго думал, куда бы запихать управление персональными ценами малой кровью.<br /> По-идее, будь я разработчиком Битела, я бы сделал это в ядре (т.к. в общем случае можно реализовать переопределение не только абонплат). Соответственно, редактирование персональных цен должно быть где-то в разделе &quot;Тарифные планы&quot; договора - например, отдельной вкладкой после &quot;Персональных тарифов&quot; и &quot;Тарифных опций&quot;.<br /> Но малой кровью модифицировать эту клиентскую панель не получилось, поэтому было решено сделать отдельную вкладку &quot;Переопределение цен&quot; на странице модуля Npay - см http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost.jpg<br /> <br /> <br /> Привожу клиентский код для 5.2<br /> Что с ним делать - решайте сами на свой страх и риск :)<br /> <br /> Добавляем нашу панель - прописываем в клиентский файл bitel/billing/module/services/npay/setup_user.properties:<br /> <br /> &lt;source lang=ini&gt;<br /> module.tab.3.class=bitel.billing.module.services.npay.CustomCostModulePanel<br /> module.tab.3.title=\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0446\u0435\u043D<br /> &lt;/source&gt;<br /> <br /> Панель:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.module.common.BGControlPanelContractSelect;<br /> import bitel.billing.module.common.BGTitleBorder;<br /> import bitel.billing.module.common.DialogToolBar;<br /> import bitel.billing.module.common.FloatTextField;<br /> import ru.bitel.bgbilling.client.common.BGEditor;<br /> import ru.bitel.bgbilling.client.common.BGUPanel;<br /> import ru.bitel.bgbilling.client.common.BGUTable;<br /> import ru.bitel.bgbilling.client.common.ClientContext;<br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.Service;<br /> import ru.bitel.bgbilling.kernel.module.common.service.ServiceService;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.client.AbstractBGUPanel;<br /> import ru.bitel.common.client.BGSwingUtilites;<br /> import ru.bitel.common.client.BGUComboBox;<br /> import ru.bitel.common.client.table.BGTableModel;<br /> import ru.bitel.common.model.KeyValue;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.swing.*;<br /> import java.awt.*;<br /> import java.awt.event.ActionEvent;<br /> import java.awt.event.ActionListener;<br /> import java.math.BigDecimal;<br /> import java.util.*;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Панелька на клиенте для редактирования переопределений стоимости тарифных планов договоров.<br /> */<br /> public class CustomCostModulePanel extends BGUPanel<br /> {<br /> private BGControlPanelContractSelect contractSelect;<br /> private ContractTariffService wsContractTariff = getContext().getPort(ContractTariffService.class, 0);<br /> <br /> private BGTableModel&lt;ContractTariff&gt; model = new BGTableModel&lt;ContractTariff&gt;(&quot;contractTariff&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, Integer.class, 0, 0, 0, &quot;id&quot;, true, false);<br /> addColumn(&quot;TPID&quot;, Integer.class, 0, 0, 0, &quot;tpid&quot;, true, false);<br /> addColumn(&quot;Название&quot;, &quot;tariffName&quot;, true);<br /> addColumn(&quot;Период&quot;, &quot;period&quot;, true);<br /> addColumn(&quot;Комментарий&quot;, &quot;comment&quot;, false);<br /> addColumn(&quot;Поз.&quot;, &quot;pos&quot;, true);<br /> }<br /> <br /> @Override<br /> public Object getValue(ContractTariff val, int column)<br /> {<br /> Object result = &quot;&quot;;<br /> switch (column)<br /> {<br /> case 3:<br /> result = TimeUtils.formatPeriod(val.getDate1(), val.getDate2());<br /> break;<br /> default:<br /> result = super.getValue(val, column);<br /> }<br /> return result;<br /> }<br /> };<br /> <br /> public CustomCostModulePanel()<br /> {<br /> super(new BorderLayout());<br /> //setLayout(new GridBagLayout());<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;refresh&quot;, &quot;Обновить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostModulePanel.this.setData();<br /> }<br /> };<br /> }<br /> <br /> public void setData() throws BGException {<br /> String cids = CustomCostModulePanel.this.contractSelect.getContracts();<br /> int cid = -1;<br /> try{<br /> cid = Utils.toIntegerList(cids).get(0);<br /> }catch(Exception ex){<br /> this.processException(ex);<br /> }<br /> <br /> CustomCostModulePanel.this.model.setData(CustomCostModulePanel.this.wsContractTariff.contractTariffList(cid));<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> setLayout(new GridBagLayout());<br /> BGUTable table = new BGUTable(this.model);<br /> <br /> JButton okButton = new JButton(&quot;OK&quot;);<br /> okButton.setMargin(new Insets(2, 2, 2, 2));<br /> okButton.addActionListener(new ActionListener() {<br /> public void actionPerformed(ActionEvent e) {<br /> try {<br /> CustomCostModulePanel.this.setData();<br /> } catch (BGException e1) {<br /> processException(e1);<br /> }<br /> }<br /> });<br /> this.contractSelect = new BGControlPanelContractSelect(true, true);<br /> this.contractSelect.add(okButton, new GridBagConstraints(2, 0, 1, 1, 0.0D, 0.0D, 10, 0,<br /> new Insets(0, 0, 5, 5), 0, 0));<br /> <br /> add(BGSwingUtilites.wrapBorder(this.contractSelect, &quot;Выберите договор&quot;), new GridBagConstraints(0, 0, 1, 1, 1.0D, 0.0D, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.setVisible(false);<br /> editor.addForm(new CustomCostTariffPanel(getContext()));<br /> add(BGSwingUtilites.wrapBorder(new JScrollPane(table), &quot;Тарифные планы договора&quot;),<br /> new GridBagConstraints(0, 1, 1, 1, 1.0D, 0.6D, 10, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 1.0D, GridBagConstraints.SOUTH, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> BGSwingUtilites.handleEdit(table, editor);<br /> }<br /> <br /> /**<br /> * Панель редактирования собственно списка переопределений услуг конкретного тарифа договора<br /> */<br /> public class CustomCostTariffPanel extends BGUPanel {<br /> private BGUTable table;<br /> CustomTariffCostService wsCustomCost = this.getContext().getPort(CustomTariffCostService.class, 0);<br /> private Map&lt;Integer, Service&gt; servicesMap;//Справочник услуг всех модулей : sid -&gt; Service<br /> private ContractTariff current;<br /> <br /> private BGTableModel&lt;CustomCostRow&gt; model = new BGTableModel&lt;CustomCostRow&gt;(&quot;customCosts&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, 0, 0, 0, null, true);<br /> addColumn(&quot;Услуга&quot;, -1, 120, -1, null, false);<br /> addColumn(&quot;Цена&quot;, BigDecimal.class, -1, 180, -1, null, false);<br /> }<br /> <br /> public Object getValue(CustomCostRow val, int column)<br /> {<br /> switch (column)<br /> {<br /> case 0:<br /> return val.sid;<br /> case 1:<br /> return val.title;<br /> case 2:<br /> return val.value;<br /> default:<br /> return super.getValue(val, column);<br /> }<br /> }<br /> };<br /> <br /> protected void initActions(){<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if ((CustomCostTariffPanel.this.current = CustomCostModulePanel.this.model.getSelectedRow()) != null)<br /> {<br /> CustomCostTariffPanel.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;Сохранить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if(CustomCostTariffPanel.this.current!=null){<br /> //CustomCostTariffPanel.this.model.getRows();<br /> List&lt;CustomTariffCost&gt; costs = new ArrayList&lt;CustomTariffCost&gt;();<br /> if(CustomCostTariffPanel.this.model.getRows()!=null){<br /> CustomTariffCost cost;<br /> for(CustomCostRow row : CustomCostTariffPanel.this.model.getRows()){<br /> cost = new CustomTariffCost(CustomCostTariffPanel.this.current.getId(),row.sid,row.value);<br /> costs.add(cost);<br /> }<br /> }<br /> CustomCostTariffPanel.this.wsCustomCost.updateCustomTariffCostList(<br /> CustomCostTariffPanel.this.current.getId(),<br /> costs<br /> );<br /> }<br /> CustomCostTariffPanel.this.performActionClose();<br /> }<br /> };<br /> }<br /> <br /> @Override<br /> public void performActionOpen()<br /> {<br /> //расставляем данные в форме редактирования, затем отображаем её<br /> setData(this.current.getId(), this.current.getTariffPlanId());<br /> super.performActionOpen();<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> //To change body of implemented methods use File | Settings | File Templates.<br /> setLayout(new GridBagLayout());<br /> this.table = new BGUTable(this.model);<br /> DialogToolBar toolBar = new DialogToolBar();<br /> <br /> ParamForm paramForm = new ParamForm(getContext());<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.addForm(paramForm);<br /> editor.setVisible(false);<br /> <br /> BGSwingUtilites.buildToolBar(toolBar, paramForm);<br /> toolBar.compact();<br /> <br /> BGSwingUtilites.handleEdit(this.table, editor);<br /> <br /> add(toolBar, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 0), 0, 0));<br /> add(new JScrollPane(this.table), new GridBagConstraints(0, 1, 1, 1, 1.0D, 1.0D, 10, 1, new Insets(0, 0,<br /> 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(5, 5, 5, 5), 0, 0));<br /> }<br /> <br /> //Сравниваем строчки цен для сортировки в таблице<br /> private final Comparator&lt;CustomCostRow&gt; COMPARATOR = new Comparator&lt;CustomCostRow&gt;()<br /> {<br /> public int compare(CustomCostRow o1, CustomCostRow o2)<br /> {<br /> int result = Integer.valueOf(o1.sid).compareTo(o2.sid);<br /> if (result != 0)<br /> {<br /> return result;<br /> }<br /> <br /> return o1.title.compareTo(o2.title);<br /> }<br /> };<br /> <br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> private BGUComboBox&lt;KeyValue&lt;Integer, String&gt;&gt; keyCombo = new BGUComboBox(<br /> KeyValue.class);<br /> <br /> public CustomCostTariffPanel(ClientContext context)<br /> {<br /> super(context);<br /> //Кэшируем список всех услуг биллинга. Если возникнет ошибка при его получении - ругаемся<br /> try {<br /> java.util.List&lt;Service&gt; serviceList = CustomCostTariffPanel.this.getContext().getPort(ServiceService.class, 0).serviceList(0);<br /> this.servicesMap = new HashMap&lt;Integer, Service&gt;(serviceList.size());<br /> for(Service s : serviceList){<br /> this.servicesMap.put(s.getId(),s);<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> }<br /> }<br /> <br /> public void setData(int id, int tpid){<br /> java.util.List&lt;CustomTariffCost&gt; costList;<br /> try {<br /> costList = wsCustomCost.customTariffCostList(id);<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> java.util.List&lt;CustomCostRow&gt; data = new ArrayList&lt;CustomCostRow&gt;(costList.size());<br /> String title;<br /> Service s;<br /> for(CustomTariffCost cost : costList){<br /> s = this.servicesMap.get(cost.getSid());<br /> if(s!=null){<br /> title = s.getTitle();<br /> }else{<br /> title = String.valueOf(cost.getSid());<br /> }<br /> data.add(new CustomCostRow(cost.getSid(), title, cost.getValue()));<br /> }<br /> Collections.sort(data, COMPARATOR);<br /> this.model.setData(data);<br /> <br /> //Получаем для комбобокса список услуг, для которых разрешено переопределение на этом тарифе<br /> java.util.List&lt;KeyValue&lt;Integer, String&gt;&gt; typeList = new ArrayList&lt;KeyValue&lt;Integer, String&gt;&gt;(CustomCostTariffPanel.this.servicesMap.size());<br /> try{<br /> for(Integer sid : this.wsCustomCost.availableServiceList(tpid)){<br /> s = this.servicesMap.get(sid);<br /> if(null!=s){<br /> title = s.getTitle();<br /> }else{<br /> title = &quot;&lt;&quot;+sid+&quot;&gt;&quot;;<br /> }<br /> <br /> typeList.add(new KeyValue&lt;Integer, String&gt;(sid, title));<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> CustomCostTariffPanel.this.keyCombo.setData(typeList);<br /> }<br /> <br /> class CustomCostRow<br /> {<br /> public int sid;<br /> public String title;<br /> public BigDecimal value;<br /> <br /> public CustomCostRow(int sid, String title, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.title = title;<br /> this.value = value;<br /> }<br /> <br /> /*public CustomCostRow(int sid, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.value = value;<br /> }*/<br /> }<br /> <br /> class ParamForm extends BGUPanel<br /> {<br /> private FloatTextField value = new FloatTextField();<br /> private CustomCostRow current;<br /> private boolean edit;<br /> <br /> public ParamForm(ClientContext context)<br /> {<br /> super(context);<br /> }<br /> <br /> protected void jbInit()<br /> {<br /> setBorder(BorderFactory.createCompoundBorder(new BGTitleBorder(&quot; Редактор &quot;), BorderFactory.createEmptyBorder(0, 3, 3, 3)));<br /> <br /> add(CustomCostTariffPanel.this.keyCombo, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 5), 0, 0));<br /> <br /> add(this.value, new GridBagConstraints(1, 0, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(0, 0, 0, 0),<br /> 0, 0));<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;new&quot;, &quot;Добавить&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = false;<br /> CustomCostTariffPanel.ParamForm.this.current = new CustomCostRow(0,&quot;&quot;,BigDecimal.ZERO);<br /> <br /> CustomCostTariffPanel.ParamForm.this.value.setText(&quot;&quot;);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(true);<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = true;<br /> <br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.keyCombo.setSelectedItem(ParamForm.this.current.sid);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(false);<br /> <br /> ParamForm.this.value.setText(String.valueOf(ParamForm.this.current.value));<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;delete&quot;, &quot;Удалить&quot;, ClientUtils.getIcon(&quot;item_delete&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> <br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.model.deleteSelectedRow();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;OK&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> KeyValue&lt;Integer, String&gt; key = CustomCostTariffPanel.this.keyCombo.getSelectedItem();<br /> if (key == null)<br /> {<br /> return;<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.current.sid = key.getKey();<br /> CustomCostTariffPanel.ParamForm.this.current.title = key.getValue();<br /> CustomCostTariffPanel.ParamForm.this.current.value = Utils.parseBigDecimal(CustomCostTariffPanel.ParamForm.this.value.getText(), BigDecimal.ZERO);<br /> <br /> if (!CustomCostTariffPanel.ParamForm.this.edit)<br /> {<br /> CustomCostTariffPanel.this.model.addRow(CustomCostTariffPanel.ParamForm.this.current);<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionClose();<br /> }<br /> };<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> Клиентские узлы тарифа:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел DayModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;за день&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;за месяц&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;за день&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;за месяц&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел MonthModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> <br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;безусловно&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;пропорц. периоду&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;2&quot;, &quot;пропорц. объему&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;3&quot;, &quot;как выгоднее&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;безусловно&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;пропорц. периоду&quot;);<br /> }<br /> else if (this.type.equals(&quot;2&quot;))<br /> {<br /> title.append(&quot;пропорц. объему&quot;);<br /> }<br /> else if (this.type.equals(&quot;3&quot;))<br /> {<br /> title.append(&quot;как выгоднее&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел YearModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Стоимость: &quot;));<br /> edit.add(this.costTf);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(&quot;Стоимость: &quot;);<br /> title.append(this.cost);<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.custom = Utils.parseBoolean((String) data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> this.costTf.setText(this.cost);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> === Отображение на договоре ===<br /> <br /> На договоре персональные цены на услуги будем писать другим шрифтом в скобках через запятую после наименования тарифа в дереве.<br /> <br /> Для этого переопределим через dynaction стандартный action вывода карточки договора:<br /> <br /> &lt;source lang=ini&gt;<br /> dynaction:contract.hierarchy.ActionContractInfo=ru.dsi.bgbilling.kernel.scripts.action.ActionContractInfo<br /> &lt;/source&gt;<br /> <br /> Переопределённый action делает много всяких штук, кроме подсветки персональных цен. Привожу целиком, лишнее можно убрать по необходимости:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.scripts.action.contract;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.ApplicationModule;<br /> import bitel.billing.server.call.bean.Login;<br /> import bitel.billing.server.contract.bean.ContractModuleManager;<br /> import bitel.billing.server.contract.bean.PersonalTariff;<br /> import bitel.billing.server.contract.bean.PersonalTariffManager;<br /> import bitel.billing.server.dialup.bean.DialUpLoginManager;<br /> import bitel.billing.server.email.bean.Account;<br /> import bitel.billing.server.email.bean.AccountManager;<br /> import bitel.billing.server.npay.bean.ServiceObject;<br /> import bitel.billing.server.npay.bean.ServiceObjectManager;<br /> import bitel.billing.server.phone.bean.ClientItem;<br /> import bitel.billing.server.phone.bean.ClientItemManager;<br /> import bitel.billing.server.voiceip.bean.VoiceIpLoginManager;<br /> import org.w3c.dom.Element;<br /> import org.w3c.dom.NodeList;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.base.server.DefaultContext;<br /> import ru.bitel.bgbilling.kernel.container.security.server.ModuleAction;<br /> import ru.bitel.bgbilling.kernel.container.security.server.PermissionChecker;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalance;<br /> import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalanceManager;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.BGModule;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.api.server.bean.InetServDao;<br /> import ru.bitel.bgbilling.modules.ipn.common.bean.AddressRange;<br /> import ru.bitel.bgbilling.modules.ipn.server.bean.AddressRangeManager;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.XMLUtils;<br /> import ru.bitel.common.worker.ThreadContext;<br /> <br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.Date;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class ActionContractInfo extends bitel.billing.server.contract.action.ActionContractInfo{<br /> @Override<br /> public void doAction() throws SQLException, BGException<br /> {<br /> super.doAction();<br /> <br /> Date now = new Date();<br /> <br /> //Удаляем модули<br /> Element modulesNode = XMLUtils.selectElement(rootNode, &quot;/data/info/modules&quot;);<br /> if(modulesNode!=null) {<br /> NodeList list = modulesNode.getChildNodes();<br /> while (modulesNode.hasChildNodes()){<br /> modulesNode.removeChild(list.item(0));<br /> }<br /> }<br /> //Количество сущностей модуля<br /> int itemCount;<br /> //Количество открытых датой сущностей модуля<br /> int openItemCount;<br /> InetServDao servDao;<br /> InetServ tree;<br /> ClientItemManager clientItemManager;<br /> DialUpLoginManager lm;<br /> ServiceObjectManager manager;<br /> AccountManager accountManager;<br /> AddressRangeManager arm;<br /> <br /> //Добавляем модули заново с доп данными:<br /> for (BGModule module : new ContractModuleManager(this.con).getContractModules(cid))<br /> {<br /> Element item = createElement(modulesNode, &quot;item&quot;);<br /> String className = module.getInstalledModule().getPackageServer() + &quot;.Module&quot;;<br /> ApplicationModule moduleClass = (ApplicationModule) Utils.newInstance(className, ApplicationModule.class);<br /> itemCount=-1;<br /> openItemCount=-1;<br /> //Inet<br /> if(moduleClass instanceof ru.bitel.bgbilling.modules.inet.api.server.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> servDao = new InetServDao(con, module.getId(), 0);<br /> tree = servDao.tree(this.cid);<br /> if(tree.getChildren()!=null){<br /> itemCount = tree.getChildren().size();<br /> for (InetServ inetServ : tree.getChildren()) {<br /> if(TimeUtils.dateBefore(now, inetServ.getDateTo()) || inetServ.getDateTo()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> servDao.recycle();<br /> }<br /> <br /> //Phone<br /> if(moduleClass instanceof bitel.billing.server.phone.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> clientItemManager = new ClientItemManager(con, module.getId());<br /> for (ClientItem clientItem : clientItemManager.getItemList(this.cid)) {<br /> itemCount++;<br /> if(TimeUtils.dateBefore(Calendar.getInstance(), clientItem.getDate2()) || clientItem.getDate2()==null){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Dialup<br /> if(moduleClass instanceof bitel.billing.server.dialup.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> lm = new DialUpLoginManager(con, module.getId());<br /> for (Login login : lm.getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Npay<br /> if(moduleClass instanceof bitel.billing.server.npay.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> manager = new ServiceObjectManager(con, module.getId());<br /> for (ServiceObject so : manager.getServiceObjectList(this.cid, null)) {<br /> itemCount++;<br /> if(so.getDate2()==null || TimeUtils.dateBefore(now, so.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //Email<br /> if(moduleClass instanceof bitel.billing.server.email.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> accountManager = new AccountManager(con, module.getId());<br /> for (Account acc : accountManager.getContractAccountList(this.cid)) {<br /> itemCount++;<br /> if(acc.getDate2()==null || TimeUtils.dateBefore(now, acc.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //IPN<br /> if(moduleClass instanceof bitel.billing.server.ipn.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> arm = new AddressRangeManager(con, module.getId());<br /> for (AddressRange range: arm.getContractRanges(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> for (AddressRange range: arm.getContractNets(this.cid, null, 0)) {<br /> itemCount++;<br /> if(range.getDate2()==null || TimeUtils.dateBefore(Calendar.getInstance(), range.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> //VoiceIP<br /> if(moduleClass instanceof bitel.billing.server.voiceip.Module){<br /> itemCount=0;<br /> openItemCount=0;<br /> for (Login login : new VoiceIpLoginManager(con, module.getId()).getContractLogins(this.cid)) {<br /> itemCount++;<br /> if(login.getDate2()==null || TimeUtils.dateBefore(now, login.getDate2())){<br /> openItemCount++;<br /> }<br /> }<br /> }<br /> <br /> item.setAttribute(&quot;id&quot;, String.valueOf(module.getId()));<br /> if(itemCount&gt;=0 &amp;&amp; openItemCount&gt;=0){<br /> item.setAttribute(&quot;title&quot;, &quot;&lt;html&gt;&quot;+module.getTitle()+&quot; &lt;font style='color: grey;' size='2'&gt;[&quot;+openItemCount+&quot;/&quot;+itemCount+&quot;]&lt;/font&gt;&lt;/htmp&gt;&quot;);<br /> }else{<br /> item.setAttribute(&quot;title&quot;, module.getTitle());<br /> }<br /> item.setAttribute(&quot;package&quot;, module.getInstalledModule().getPackageClient());<br /> if (moduleClass != null)<br /> {<br /> item.setAttribute(&quot;status&quot;, moduleClass.getStatus(this.con, module.getId(), cid));<br /> }<br /> }<br /> <br /> Element tariff = XMLUtils.selectElement(rootNode, &quot;/data/info/tariff&quot;);<br /> //Удаляем все тарифы из списка - будем строить свой список<br /> if(tariff!=null){<br /> NodeList list = tariff.getChildNodes();<br /> while(tariff.hasChildNodes()){<br /> tariff.removeChild(list.item(0));<br /> }<br /> /*for(int i = 0;i&lt;list.getLength();i++){<br /> tariff.removeChild(list.item(i));<br /> }*/<br /> //Составляем свой список тарифов с красивостями<br /> Map&lt;String, String&gt; mapRequest = new HashMap&lt;String, String&gt;();<br /> mapRequest.put(&quot;action&quot;, &quot;PersonalTariffTable&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract.tariff&quot;);<br /> <br /> ModuleAction action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> for (PersonalTariff personalTariff : new PersonalTariffManager(this.con)<br /> .getPersonalTariffList(cid, new Date())) {<br /> addListItem(tariff, personalTariff.getId(), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;&quot; + &quot;ПТ: &quot; + personalTariff.getTitle()+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> mapRequest.clear();<br /> mapRequest.put(&quot;action&quot;, &quot;ContractTariffPlans&quot;);<br /> mapRequest.put(&quot;module&quot;, &quot;contract&quot;);<br /> <br /> action = PermissionChecker.getInstance().findAction(new String[] { &quot;0&quot; }, mapRequest);<br /> if ((Setup.getSetup().getInt(&quot;bgsecure.check&quot;, 1) == 0) ||<br /> (action == null) || (this.userID == 1) ||<br /> (PermissionChecker.getInstance().checkActionAllow(mapRequest, this.con, &quot;0&quot;, action, this.userID, cid) == null))<br /> {<br /> String query = &quot;SELECT t2.id, t2.title, group_concat(TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM ctc.value))) value, t1.date1 FROM contract_tariff AS t1 left join tariff_plan AS t2 on t1.tpid=t2.id left join custom_tariff_cost ctc on t1.id=ctc.contract_tariff_id &quot; +<br /> &quot; WHERE t1.cid=? AND emid=0 AND eid=0 AND ( isNull( date2 ) OR date2&gt;=CURDATE() ) group by t1.id&quot;;<br /> //AND (isNull( date1 ) OR date1&lt;=CURDATE())<br /> PreparedStatement ps = this.con.prepareStatement(query);<br /> ps.setInt(1, cid);<br /> ResultSet rs = ps.executeQuery();<br /> String value;<br /> Date dt1;<br /> String name;<br /> while (rs.next())<br /> {<br /> value = rs.getString(3);<br /> name = rs.getString(2);<br /> //21.12.2016 by semyon@dsi.ru - show future tariffs too, but with another color<br /> dt1=TimeUtils.convertSqlDateToDate(rs.getDate(4));<br /> if(TimeUtils.dateBefore(now,dt1)){<br /> name = &quot;&lt;font style='color: #cc99ff;'&gt;&quot;+name+&quot;&lt;/font&gt;&quot;;<br /> }<br /> if(value==null){<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&quot;+name+&quot;&lt;/html&gt;&quot;);<br /> }else{<br /> addListItem(tariff, rs.getInt(1), &quot;&lt;html&gt;&lt;font style='color: green;'&gt;*&lt;/font&gt; &quot;+name+&quot; &lt;font style='color: green;'&gt;(&quot;+value+&quot;)&lt;/font&gt;&lt;/html&gt;&quot; );<br /> }<br /> }<br /> rs.close();<br /> ps.close();<br /> }<br /> }<br /> <br /> //Подсвечиваем статус &quot;закрыт&quot;<br /> Element contractElement = XMLUtils.selectElement(rootNode, &quot;contract&quot;);<br /> if(contractElement!=null){<br /> if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;закрыт&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;закрыт&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;приостановлен&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: blue;'&gt;приостановлен&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }else if(contractElement.getAttribute(&quot;status&quot;).equals(&quot;в отключении&quot;)){<br /> contractElement.setAttribute(&quot;status&quot;, &quot;&lt;html&gt;&lt;font style='color: #5F84B6;'&gt;в отключении&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> <br /> //Проверяем баланс и лимит на договоре - выделяем лимит красным, если баланс&lt;лимита<br /> if(contractElement!=null){<br /> ConvergenceBalanceManager convergenceBalanceManager = ConvergenceBalanceManager.getInstance();<br /> if (convergenceBalanceManager != null) {<br /> ConvergenceBalance balance = convergenceBalanceManager.getBalance((ThreadContext.get(DefaultContext.class)).getConnectionSet(), this.cid, System.currentTimeMillis());<br /> if(balance!=null &amp;&amp; !balance.isBalanceUnderLimit()){<br /> contractElement.setAttribute(&quot;limit&quot;, &quot;&lt;html&gt;&lt;font style='color: red;'&gt;&quot;+contractElement.getAttribute(&quot;limit&quot;)+&quot;&lt;/font&gt;&lt;/html&gt;&quot;);<br /> }<br /> }<br /> }<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> == Заключение ==</div> Mon, 08 May 2017 07:41:47 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Клиетский интерфейс */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> <br /> Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.<br /> <br /> ==== Веб-сервис переопределения цен ====<br /> <br /> Вспомогательные классы:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import ru.bitel.common.model.Id;<br /> <br /> import java.io.Serializable;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Тарифный план договора для использования в веб-сервисе.<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя<br /> *<br /> */<br /> public class ContractTariff extends Id implements Serializable {<br /> private int contractId;<br /> private int tariffPlanId;<br /> private Date date1;<br /> private Date date2;<br /> private String comment = &quot;&quot;;<br /> private String tariffName;<br /> private int pos;<br /> private int entityMid;<br /> private int entityId;<br /> private int replacedFromContractTariffId;<br /> <br /> public ContractTariff() {<br /> this.id = -1;<br /> this.contractId = -1;<br /> this.tariffPlanId = -1;<br /> this.date1 = null;<br /> this.date2 = null;<br /> this.tariffName = null;<br /> this.pos = 0;<br /> this.entityId = 0;<br /> this.entityMid = 0;<br /> this.replacedFromContractTariffId = -1;<br /> }<br /> <br /> public int getContractId() {<br /> return contractId;<br /> }<br /> <br /> public void setContractId(int contractId) {<br /> this.contractId = contractId;<br /> }<br /> <br /> public int getTariffPlanId() {<br /> return tariffPlanId;<br /> }<br /> <br /> public String getTariffName() {<br /> return tariffName;<br /> }<br /> <br /> public void setTariffName(String tariffName) {<br /> this.tariffName = tariffName;<br /> }<br /> <br /> public void setTariffPlanId(int tariffPlanId) {<br /> this.tariffPlanId = tariffPlanId;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> <br /> public String getComment() {<br /> return comment;<br /> }<br /> <br /> public void setComment(String comment) {<br /> this.comment = comment;<br /> }<br /> <br /> public int getPos() {<br /> return pos;<br /> }<br /> <br /> public void setPos(int pos) {<br /> this.pos = pos;<br /> }<br /> <br /> public int getEntityMid() {<br /> return entityMid;<br /> }<br /> <br /> public void setEntityMid(int entityMid) {<br /> this.entityMid = entityMid;<br /> }<br /> <br /> public int getEntityId() {<br /> return entityId;<br /> }<br /> <br /> public void setEntityId(int entityId) {<br /> this.entityId = entityId;<br /> }<br /> <br /> public int getReplacedFromContractTariffId() {<br /> return replacedFromContractTariffId;<br /> }<br /> <br /> public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {<br /> this.replacedFromContractTariffId = replacedFromContractTariffId;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом<br /> *<br /> */<br /> @WebService<br /> @XmlSeeAlso({ContractTariff.class})<br /> public abstract interface ContractTariffService {<br /> @WebMethod<br /> public abstract List&lt;ContractTariff&gt; contractTariffList(@WebParam(name=&quot;contractId&quot;) int contracId) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.util.List;<br /> <br /> /**<br /> * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService&quot;)<br /> public class ContractTariffServiceImpl extends AbstractService<br /> implements ContractTariffService {<br /> <br /> @Override<br /> public List&lt;ContractTariff&gt; contractTariffList(@WebParam(name = &quot;contractId&quot;) int contracId) throws BGException {<br /> return (new ContractTariffDao(getConnection()).list(contracId));<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Веб-сервис работы с переопределёнными ценами:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Интерфейс веб-сервиса для работы с персональными ценами<br /> */<br /> @WebService<br /> @XmlSeeAlso({CustomTariffCost.class})<br /> public abstract interface CustomTariffCostService {<br /> <br /> @WebMethod<br /> public abstract List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id)<br /> throws BGException;<br /> <br /> @WebMethod<br /> public abstract void updateCustomTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs)<br /> throws BGException;<br /> <br /> /**<br /> * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> */<br /> @WebMethod<br /> public abstract Set&lt;Integer&gt; availableServiceList(@WebParam(name=&quot;tariffPlanId&quot;) int tpid) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;<br /> import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;<br /> import ru.bitel.bgbilling.server.util.ClosedDateChecker;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.List;<br /> import java.util.Set;<br /> import java.util.TreeSet;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * Реализация веб-сервиса для работы с персональными ценам<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService&quot;)<br /> public class CustomTariffCostServiceImpl extends AbstractService<br /> implements CustomTariffCostService {<br /> <br /> @Override<br /> public List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name = &quot;contractTariffId&quot;) int contract_tariff_id) throws BGException {<br /> return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);<br /> }<br /> <br /> @Override<br /> public void updateCustomTariffCostList(@WebParam(name = &quot;contractTariffId&quot;)int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs) throws BGException{<br /> <br /> Calendar closedDate;<br /> ContractTariffManager ctm = new ContractTariffManager(getConnection());<br /> ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);<br /> if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking(&quot;updateCustomTariffCostList&quot;, 0, this.userId)) != null)<br /> {<br /> ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());<br /> }<br /> CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());<br /> try {<br /> ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);<br /> } catch (SQLException e) {<br /> throw new BGException(e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> * На данный момент реализовано через метки - ищем все метки тарифного плана вида &quot;id - Название&quot;,<br /> * где id - это id услуги. Например &quot;5 - Интернет&quot;.<br /> */<br /> @Override<br /> public Set&lt;Integer&gt; availableServiceList(@WebParam(name = &quot;tariffPlanId&quot;) int tpid)<br /> throws BGException<br /> {<br /> Set&lt;Integer&gt; result = new TreeSet&lt;Integer&gt;();<br /> TariffLabelManager tlm = new TariffLabelManager(getConnection());<br /> //Получаем список id меток тарифа<br /> Set&lt;Integer&gt; tariffLabelIds = tlm.getTariffLabelIds(tpid);<br /> if(tariffLabelIds.size()&gt;0){<br /> //Берём список всех меток с названиями<br /> List&lt;TariffLabelItem&gt; tariffLabelItemList = tlm.getTariffLabelItemList();<br /> //Ищем среди меток тарифа метки с названиями вида &quot;id - Название&quot;<br /> /*for(Integer label_id : tariffLabelIds){<br /> TariffLabelItem label = tariffLabelItemList.get()<br /> }*/<br /> Pattern pattern = Pattern.compile(&quot;^(\\d+) - .+$&quot;);<br /> <br /> Matcher matcher;<br /> //Перебираем все метки из справочника<br /> for(TariffLabelItem label : tariffLabelItemList){<br /> //Если метка есть на нашем тарифе ...<br /> if(tariffLabelIds.contains(label.getId())){<br /> //...то смотрим её название<br /> matcher = pattern.matcher(label.getTitle());<br /> if(matcher.find()){//определился sid услуги - отлично, добавляем в результат<br /> try{<br /> result.add(Integer.valueOf(matcher.group(1)));<br /> }catch (Exception e){}<br /> }<br /> }<br /> }<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> ==== Клиетский интерфейс ====<br /> <br /> Для начала немного костылей.<br /> <br /> Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:<br /> <br /> [[Файл:custom_cost_labels.jpg]]<br /> <br /> Т.е. чтобы услуга появилась в интерфейсе, нужно завести метку вида &quot;xxx - Наименование&quot;, где xxx - sid услуги, дополненный нулями до 3 символов, и проставить эту метку для всех необходимых тарифов.<br /> <br /> Переходим собственно к интерфейсу.<br /> <br /> Менять клиентский интерфейс биллинга стандартным образом нельзя - только через переопределение jar-файлов клиента.<br /> <br /> Я долго думал, куда бы запихать управление персональными ценами малой кровью.<br /> По-идее, будь я разработчиком Битела, я бы сделал это в ядре (т.к. в общем случае можно реализовать переопределение не только абонплат). Соответственно, редактирование персональных цен должно быть где-то в разделе &quot;Тарифные планы&quot; договора - например, отдельной вкладкой после &quot;Персональных тарифов&quot; и &quot;Тарифных опций&quot;.<br /> Но малой кровью модифицировать эту клиентскую панель не получилось, поэтому было решено сделать отдельную вкладку &quot;Переопределение цен&quot; на странице модуля Npay - см http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost.jpg<br /> <br /> <br /> Привожу клиентский код для 5.2<br /> Что с ним делать - решайте сами на свой страх и риск :)<br /> <br /> Добавляем нашу панель - прописываем в клиентский файл bitel/billing/module/services/npay/setup_user.properties:<br /> <br /> &lt;source lang=ini&gt;<br /> module.tab.3.class=bitel.billing.module.services.npay.CustomCostModulePanel<br /> module.tab.3.title=\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0446\u0435\u043D<br /> &lt;/source&gt;<br /> <br /> Панель:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.module.common.BGControlPanelContractSelect;<br /> import bitel.billing.module.common.BGTitleBorder;<br /> import bitel.billing.module.common.DialogToolBar;<br /> import bitel.billing.module.common.FloatTextField;<br /> import ru.bitel.bgbilling.client.common.BGEditor;<br /> import ru.bitel.bgbilling.client.common.BGUPanel;<br /> import ru.bitel.bgbilling.client.common.BGUTable;<br /> import ru.bitel.bgbilling.client.common.ClientContext;<br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.module.common.bean.Service;<br /> import ru.bitel.bgbilling.kernel.module.common.service.ServiceService;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.client.AbstractBGUPanel;<br /> import ru.bitel.common.client.BGSwingUtilites;<br /> import ru.bitel.common.client.BGUComboBox;<br /> import ru.bitel.common.client.table.BGTableModel;<br /> import ru.bitel.common.model.KeyValue;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.swing.*;<br /> import java.awt.*;<br /> import java.awt.event.ActionEvent;<br /> import java.awt.event.ActionListener;<br /> import java.math.BigDecimal;<br /> import java.util.*;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Панелька на клиенте для редактирования переопределений стоимости тарифных планов договоров.<br /> */<br /> public class CustomCostModulePanel extends BGUPanel<br /> {<br /> private BGControlPanelContractSelect contractSelect;<br /> private ContractTariffService wsContractTariff = getContext().getPort(ContractTariffService.class, 0);<br /> <br /> private BGTableModel&lt;ContractTariff&gt; model = new BGTableModel&lt;ContractTariff&gt;(&quot;contractTariff&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, Integer.class, 0, 0, 0, &quot;id&quot;, true, false);<br /> addColumn(&quot;TPID&quot;, Integer.class, 0, 0, 0, &quot;tpid&quot;, true, false);<br /> addColumn(&quot;Название&quot;, &quot;tariffName&quot;, true);<br /> addColumn(&quot;Период&quot;, &quot;period&quot;, true);<br /> addColumn(&quot;Комментарий&quot;, &quot;comment&quot;, false);<br /> addColumn(&quot;Поз.&quot;, &quot;pos&quot;, true);<br /> }<br /> <br /> @Override<br /> public Object getValue(ContractTariff val, int column)<br /> {<br /> Object result = &quot;&quot;;<br /> switch (column)<br /> {<br /> case 3:<br /> result = TimeUtils.formatPeriod(val.getDate1(), val.getDate2());<br /> break;<br /> default:<br /> result = super.getValue(val, column);<br /> }<br /> return result;<br /> }<br /> };<br /> <br /> public CustomCostModulePanel()<br /> {<br /> super(new BorderLayout());<br /> //setLayout(new GridBagLayout());<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;refresh&quot;, &quot;Обновить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostModulePanel.this.setData();<br /> }<br /> };<br /> }<br /> <br /> public void setData() throws BGException {<br /> String cids = CustomCostModulePanel.this.contractSelect.getContracts();<br /> int cid = -1;<br /> try{<br /> cid = Utils.toIntegerList(cids).get(0);<br /> }catch(Exception ex){<br /> this.processException(ex);<br /> }<br /> <br /> CustomCostModulePanel.this.model.setData(CustomCostModulePanel.this.wsContractTariff.contractTariffList(cid));<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> setLayout(new GridBagLayout());<br /> BGUTable table = new BGUTable(this.model);<br /> <br /> JButton okButton = new JButton(&quot;OK&quot;);<br /> okButton.setMargin(new Insets(2, 2, 2, 2));<br /> okButton.addActionListener(new ActionListener() {<br /> public void actionPerformed(ActionEvent e) {<br /> try {<br /> CustomCostModulePanel.this.setData();<br /> } catch (BGException e1) {<br /> processException(e1);<br /> }<br /> }<br /> });<br /> this.contractSelect = new BGControlPanelContractSelect(true, true);<br /> this.contractSelect.add(okButton, new GridBagConstraints(2, 0, 1, 1, 0.0D, 0.0D, 10, 0,<br /> new Insets(0, 0, 5, 5), 0, 0));<br /> <br /> add(BGSwingUtilites.wrapBorder(this.contractSelect, &quot;Выберите договор&quot;), new GridBagConstraints(0, 0, 1, 1, 1.0D, 0.0D, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.setVisible(false);<br /> editor.addForm(new CustomCostTariffPanel(getContext()));<br /> add(BGSwingUtilites.wrapBorder(new JScrollPane(table), &quot;Тарифные планы договора&quot;),<br /> new GridBagConstraints(0, 1, 1, 1, 1.0D, 0.6D, 10, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 1.0D, GridBagConstraints.SOUTH, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));<br /> BGSwingUtilites.handleEdit(table, editor);<br /> }<br /> <br /> /**<br /> * Панель редактирования собственно списка переопределений услуг конкретного тарифа договора<br /> */<br /> public class CustomCostTariffPanel extends BGUPanel {<br /> private BGUTable table;<br /> CustomTariffCostService wsCustomCost = this.getContext().getPort(CustomTariffCostService.class, 0);<br /> private Map&lt;Integer, Service&gt; servicesMap;//Справочник услуг всех модулей : sid -&gt; Service<br /> private ContractTariff current;<br /> <br /> private BGTableModel&lt;CustomCostRow&gt; model = new BGTableModel&lt;CustomCostRow&gt;(&quot;customCosts&quot;)<br /> {<br /> protected void initColumns()<br /> {<br /> addColumn(&quot;ID&quot;, 0, 0, 0, null, true);<br /> addColumn(&quot;Услуга&quot;, -1, 120, -1, null, false);<br /> addColumn(&quot;Цена&quot;, BigDecimal.class, -1, 180, -1, null, false);<br /> }<br /> <br /> public Object getValue(CustomCostRow val, int column)<br /> {<br /> switch (column)<br /> {<br /> case 0:<br /> return val.sid;<br /> case 1:<br /> return val.title;<br /> case 2:<br /> return val.value;<br /> default:<br /> return super.getValue(val, column);<br /> }<br /> }<br /> };<br /> <br /> protected void initActions(){<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if ((CustomCostTariffPanel.this.current = CustomCostModulePanel.this.model.getSelectedRow()) != null)<br /> {<br /> CustomCostTariffPanel.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;Сохранить&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> if(CustomCostTariffPanel.this.current!=null){<br /> //CustomCostTariffPanel.this.model.getRows();<br /> List&lt;CustomTariffCost&gt; costs = new ArrayList&lt;CustomTariffCost&gt;();<br /> if(CustomCostTariffPanel.this.model.getRows()!=null){<br /> CustomTariffCost cost;<br /> for(CustomCostRow row : CustomCostTariffPanel.this.model.getRows()){<br /> cost = new CustomTariffCost(CustomCostTariffPanel.this.current.getId(),row.sid,row.value);<br /> costs.add(cost);<br /> }<br /> }<br /> CustomCostTariffPanel.this.wsCustomCost.updateCustomTariffCostList(<br /> CustomCostTariffPanel.this.current.getId(),<br /> costs<br /> );<br /> }<br /> CustomCostTariffPanel.this.performActionClose();<br /> }<br /> };<br /> }<br /> <br /> @Override<br /> public void performActionOpen()<br /> {<br /> //расставляем данные в форме редактирования, затем отображаем её<br /> setData(this.current.getId(), this.current.getTariffPlanId());<br /> super.performActionOpen();<br /> }<br /> <br /> @Override<br /> protected void jbInit() throws Exception {<br /> //To change body of implemented methods use File | Settings | File Templates.<br /> setLayout(new GridBagLayout());<br /> this.table = new BGUTable(this.model);<br /> DialogToolBar toolBar = new DialogToolBar();<br /> <br /> ParamForm paramForm = new ParamForm(getContext());<br /> <br /> BGEditor editor = new BGEditor();<br /> editor.addForm(paramForm);<br /> editor.setVisible(false);<br /> <br /> BGSwingUtilites.buildToolBar(toolBar, paramForm);<br /> toolBar.compact();<br /> <br /> BGSwingUtilites.handleEdit(this.table, editor);<br /> <br /> add(toolBar, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 0), 0, 0));<br /> add(new JScrollPane(this.table), new GridBagConstraints(0, 1, 1, 1, 1.0D, 1.0D, 10, 1, new Insets(0, 0,<br /> 0, 0), 0, 0));<br /> add(editor, new GridBagConstraints(0, 2, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(5, 5, 5, 5), 0, 0));<br /> }<br /> <br /> //Сравниваем строчки цен для сортировки в таблице<br /> private final Comparator&lt;CustomCostRow&gt; COMPARATOR = new Comparator&lt;CustomCostRow&gt;()<br /> {<br /> public int compare(CustomCostRow o1, CustomCostRow o2)<br /> {<br /> int result = Integer.valueOf(o1.sid).compareTo(o2.sid);<br /> if (result != 0)<br /> {<br /> return result;<br /> }<br /> <br /> return o1.title.compareTo(o2.title);<br /> }<br /> };<br /> <br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> private BGUComboBox&lt;KeyValue&lt;Integer, String&gt;&gt; keyCombo = new BGUComboBox(<br /> KeyValue.class);<br /> <br /> public CustomCostTariffPanel(ClientContext context)<br /> {<br /> super(context);<br /> //Кэшируем список всех услуг биллинга. Если возникнет ошибка при его получении - ругаемся<br /> try {<br /> java.util.List&lt;Service&gt; serviceList = CustomCostTariffPanel.this.getContext().getPort(ServiceService.class, 0).serviceList(0);<br /> this.servicesMap = new HashMap&lt;Integer, Service&gt;(serviceList.size());<br /> for(Service s : serviceList){<br /> this.servicesMap.put(s.getId(),s);<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> }<br /> }<br /> <br /> public void setData(int id, int tpid){<br /> java.util.List&lt;CustomTariffCost&gt; costList;<br /> try {<br /> costList = wsCustomCost.customTariffCostList(id);<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> java.util.List&lt;CustomCostRow&gt; data = new ArrayList&lt;CustomCostRow&gt;(costList.size());<br /> String title;<br /> Service s;<br /> for(CustomTariffCost cost : costList){<br /> s = this.servicesMap.get(cost.getSid());<br /> if(s!=null){<br /> title = s.getTitle();<br /> }else{<br /> title = String.valueOf(cost.getSid());<br /> }<br /> data.add(new CustomCostRow(cost.getSid(), title, cost.getValue()));<br /> }<br /> Collections.sort(data, COMPARATOR);<br /> this.model.setData(data);<br /> <br /> //Получаем для комбобокса список услуг, для которых разрешено переопределение на этом тарифе<br /> java.util.List&lt;KeyValue&lt;Integer, String&gt;&gt; typeList = new ArrayList&lt;KeyValue&lt;Integer, String&gt;&gt;(CustomCostTariffPanel.this.servicesMap.size());<br /> try{<br /> for(Integer sid : this.wsCustomCost.availableServiceList(tpid)){<br /> s = this.servicesMap.get(sid);<br /> if(null!=s){<br /> title = s.getTitle();<br /> }else{<br /> title = &quot;&lt;&quot;+sid+&quot;&gt;&quot;;<br /> }<br /> <br /> typeList.add(new KeyValue&lt;Integer, String&gt;(sid, title));<br /> }<br /> } catch (BGException e) {<br /> this.processException(e);<br /> return;<br /> }<br /> CustomCostTariffPanel.this.keyCombo.setData(typeList);<br /> }<br /> <br /> class CustomCostRow<br /> {<br /> public int sid;<br /> public String title;<br /> public BigDecimal value;<br /> <br /> public CustomCostRow(int sid, String title, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.title = title;<br /> this.value = value;<br /> }<br /> <br /> /*public CustomCostRow(int sid, BigDecimal value)<br /> {<br /> this.sid = sid;<br /> this.value = value;<br /> }*/<br /> }<br /> <br /> class ParamForm extends BGUPanel<br /> {<br /> private FloatTextField value = new FloatTextField();<br /> private CustomCostRow current;<br /> private boolean edit;<br /> <br /> public ParamForm(ClientContext context)<br /> {<br /> super(context);<br /> }<br /> <br /> protected void jbInit()<br /> {<br /> setBorder(BorderFactory.createCompoundBorder(new BGTitleBorder(&quot; Редактор &quot;), BorderFactory.createEmptyBorder(0, 3, 3, 3)));<br /> <br /> add(CustomCostTariffPanel.this.keyCombo, new GridBagConstraints(0, 0, 1, 1, 0.0D, 0.0D, 17, 0, new Insets(0, 0, 0, 5), 0, 0));<br /> <br /> add(this.value, new GridBagConstraints(1, 0, 1, 1, 1.0D, 0.0D, 10, 2, new Insets(0, 0, 0, 0),<br /> 0, 0));<br /> }<br /> <br /> protected void initActions()<br /> {<br /> new AbstractBGUPanel.DefaultAction(&quot;new&quot;, &quot;Добавить&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = false;<br /> CustomCostTariffPanel.ParamForm.this.current = new CustomCostRow(0,&quot;&quot;,BigDecimal.ZERO);<br /> <br /> CustomCostTariffPanel.ParamForm.this.value.setText(&quot;&quot;);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(true);<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;edit&quot;, &quot;Редактировать&quot;, ClientUtils.getIcon(&quot;item_edit&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.edit = true;<br /> <br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.keyCombo.setSelectedItem(ParamForm.this.current.sid);<br /> CustomCostTariffPanel.this.keyCombo.setEnabled(false);<br /> <br /> ParamForm.this.value.setText(String.valueOf(ParamForm.this.current.value));<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionOpen();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;delete&quot;, &quot;Удалить&quot;, ClientUtils.getIcon(&quot;item_delete&quot;))<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> CustomCostTariffPanel.ParamForm.this.current = CustomCostTariffPanel.this.model.getSelectedRow();<br /> <br /> if (CustomCostTariffPanel.ParamForm.this.current != null)<br /> {<br /> CustomCostTariffPanel.this.model.deleteSelectedRow();<br /> }<br /> }<br /> };<br /> new AbstractBGUPanel.DefaultAction(&quot;ok&quot;, &quot;OK&quot;)<br /> {<br /> public void actionPerformedImpl(ActionEvent e)<br /> throws Exception<br /> {<br /> KeyValue&lt;Integer, String&gt; key = CustomCostTariffPanel.this.keyCombo.getSelectedItem();<br /> if (key == null)<br /> {<br /> return;<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.current.sid = key.getKey();<br /> CustomCostTariffPanel.ParamForm.this.current.title = key.getValue();<br /> CustomCostTariffPanel.ParamForm.this.current.value = Utils.parseBigDecimal(CustomCostTariffPanel.ParamForm.this.value.getText(), BigDecimal.ZERO);<br /> <br /> if (!CustomCostTariffPanel.ParamForm.this.edit)<br /> {<br /> CustomCostTariffPanel.this.model.addRow(CustomCostTariffPanel.ParamForm.this.current);<br /> }<br /> <br /> CustomCostTariffPanel.ParamForm.this.performActionClose();<br /> }<br /> };<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> Клиентские узлы тарифа:<br /> &lt;source lang=java&gt;<br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел DayModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;за день&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;за месяц&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;за день&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;за месяц&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.BGComboBox;<br /> import bitel.billing.module.common.ComboBoxItem;<br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел MonthModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private String type;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private BGComboBox typeCombo;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> <br /> this.typeCombo = new BGComboBox();<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;0&quot;, &quot;безусловно&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;1&quot;, &quot;пропорц. периоду&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;2&quot;, &quot;пропорц. объему&quot;));<br /> this.typeCombo.addItem(new ComboBoxItem(&quot;3&quot;, &quot;как выгоднее&quot;));<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Цена &quot;));<br /> edit.add(this.costTf);<br /> edit.add(new JLabel(&quot; начислять &quot;));<br /> edit.add(this.typeCombo);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(this.cost);<br /> title.append(&quot; &quot;);<br /> <br /> if (this.type.equals(&quot;0&quot;))<br /> {<br /> title.append(&quot;безусловно&quot;);<br /> }<br /> else if (this.type.equals(&quot;1&quot;))<br /> {<br /> title.append(&quot;пропорц. периоду&quot;);<br /> }<br /> else if (this.type.equals(&quot;2&quot;))<br /> {<br /> title.append(&quot;пропорц. объему&quot;);<br /> }<br /> else if (this.type.equals(&quot;3&quot;))<br /> {<br /> title.append(&quot;как выгоднее&quot;);<br /> }<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.type = Utils.maskNull((String)data.get(&quot;type&quot;));<br /> this.custom = Utils.parseBoolean((String)data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> <br /> this.costTf.setText(this.cost);<br /> ClientUtils.setComboBoxSelection(this.typeCombo, this.type);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;type&quot;, ClientUtils.getIdFromComboBox(this.typeCombo));<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> package bitel.billing.module.services.npay;<br /> <br /> import bitel.billing.module.common.FloatTextField;<br /> import bitel.billing.module.tariff.DefaultTariffTreeNode;<br /> import java.awt.Component;<br /> import java.util.HashMap;<br /> import java.util.Map;<br /> import javax.swing.*;<br /> <br /> import ru.bitel.bgbilling.client.util.ClientUtils;<br /> import ru.bitel.common.Utils;<br /> <br /> /**<br /> * Стандартный узел YearModeCostTariffTreeNode на 22.03.2013, с добавлением чекбокса &quot;Разрешено переопределение в договоре&quot;<br /> * (custom=0|1 [default=0])<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends DefaultTariffTreeNode<br /> {<br /> private static Icon icon = ClientUtils.getIcon(&quot;coin&quot;);<br /> private JLabel view;<br /> private String cost;<br /> private boolean custom;<br /> private FloatTextField costTf;<br /> private JCheckBox customChb;<br /> <br /> private void initEdit()<br /> {<br /> if (this.costTf == null)<br /> {<br /> this.costTf = new FloatTextField();<br /> this.customChb = new JCheckBox(&quot;Разрешено переопределение в договоре&quot;);<br /> }<br /> }<br /> <br /> protected JPanel getEditorPanel()<br /> {<br /> initEdit();<br /> <br /> JPanel edit = new JPanel();<br /> <br /> edit.add(new JLabel(&quot;Стоимость: &quot;));<br /> edit.add(this.costTf);<br /> edit.add(this.customChb);<br /> <br /> return edit;<br /> }<br /> <br /> public Component getView()<br /> {<br /> if (this.view == null)<br /> {<br /> this.view = new JLabel(icon, SwingConstants.LEFT);<br /> }<br /> <br /> extractData();<br /> <br /> StringBuilder title = new StringBuilder();<br /> <br /> title.append(&quot;Стоимость: &quot;);<br /> title.append(this.cost);<br /> <br /> if (this.custom)<br /> {<br /> title.append(&quot; (возможно переопределение)&quot;);<br /> }<br /> <br /> this.view.setText(title.toString());<br /> <br /> return this.view;<br /> }<br /> <br /> private void extractData()<br /> {<br /> Map data = getDataInHash();<br /> this.cost = ((String)data.get(&quot;cost&quot;));<br /> this.custom = Utils.parseBoolean((String) data.get(&quot;custom&quot;), false);<br /> }<br /> <br /> protected void loadData()<br /> {<br /> extractData();<br /> this.costTf.setText(this.cost);<br /> this.customChb.setSelected(this.custom);<br /> }<br /> <br /> protected void serializeData()<br /> {<br /> Map&lt;String, String&gt; data = new HashMap&lt;String, String&gt;();<br /> data.put(&quot;cost&quot;, this.costTf.getText());<br /> data.put(&quot;custom&quot;, Utils.booleanToStringInt(this.customChb.isSelected()));<br /> setDataInHash(data);<br /> }<br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> === Отображение на договоре ===<br /> <br /> == Заключение ==</div> Mon, 08 May 2017 07:04:57 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Файл:Custom cost labels.jpg http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost_labels.jpg <p>Cromeshnic:&#32;</p> <hr /> <div></div> Mon, 08 May 2017 06:51:46 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%84%D0%B0%D0%B9%D0%BB%D0%B0:Custom_cost_labels.jpg Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Клиетский интерфейс */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> <br /> Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.<br /> <br /> ==== Веб-сервис переопределения цен ====<br /> <br /> Вспомогательные классы:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import ru.bitel.common.model.Id;<br /> <br /> import java.io.Serializable;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Тарифный план договора для использования в веб-сервисе.<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя<br /> *<br /> */<br /> public class ContractTariff extends Id implements Serializable {<br /> private int contractId;<br /> private int tariffPlanId;<br /> private Date date1;<br /> private Date date2;<br /> private String comment = &quot;&quot;;<br /> private String tariffName;<br /> private int pos;<br /> private int entityMid;<br /> private int entityId;<br /> private int replacedFromContractTariffId;<br /> <br /> public ContractTariff() {<br /> this.id = -1;<br /> this.contractId = -1;<br /> this.tariffPlanId = -1;<br /> this.date1 = null;<br /> this.date2 = null;<br /> this.tariffName = null;<br /> this.pos = 0;<br /> this.entityId = 0;<br /> this.entityMid = 0;<br /> this.replacedFromContractTariffId = -1;<br /> }<br /> <br /> public int getContractId() {<br /> return contractId;<br /> }<br /> <br /> public void setContractId(int contractId) {<br /> this.contractId = contractId;<br /> }<br /> <br /> public int getTariffPlanId() {<br /> return tariffPlanId;<br /> }<br /> <br /> public String getTariffName() {<br /> return tariffName;<br /> }<br /> <br /> public void setTariffName(String tariffName) {<br /> this.tariffName = tariffName;<br /> }<br /> <br /> public void setTariffPlanId(int tariffPlanId) {<br /> this.tariffPlanId = tariffPlanId;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> <br /> public String getComment() {<br /> return comment;<br /> }<br /> <br /> public void setComment(String comment) {<br /> this.comment = comment;<br /> }<br /> <br /> public int getPos() {<br /> return pos;<br /> }<br /> <br /> public void setPos(int pos) {<br /> this.pos = pos;<br /> }<br /> <br /> public int getEntityMid() {<br /> return entityMid;<br /> }<br /> <br /> public void setEntityMid(int entityMid) {<br /> this.entityMid = entityMid;<br /> }<br /> <br /> public int getEntityId() {<br /> return entityId;<br /> }<br /> <br /> public void setEntityId(int entityId) {<br /> this.entityId = entityId;<br /> }<br /> <br /> public int getReplacedFromContractTariffId() {<br /> return replacedFromContractTariffId;<br /> }<br /> <br /> public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {<br /> this.replacedFromContractTariffId = replacedFromContractTariffId;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом<br /> *<br /> */<br /> @WebService<br /> @XmlSeeAlso({ContractTariff.class})<br /> public abstract interface ContractTariffService {<br /> @WebMethod<br /> public abstract List&lt;ContractTariff&gt; contractTariffList(@WebParam(name=&quot;contractId&quot;) int contracId) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.util.List;<br /> <br /> /**<br /> * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService&quot;)<br /> public class ContractTariffServiceImpl extends AbstractService<br /> implements ContractTariffService {<br /> <br /> @Override<br /> public List&lt;ContractTariff&gt; contractTariffList(@WebParam(name = &quot;contractId&quot;) int contracId) throws BGException {<br /> return (new ContractTariffDao(getConnection()).list(contracId));<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Веб-сервис работы с переопределёнными ценами:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Интерфейс веб-сервиса для работы с персональными ценами<br /> */<br /> @WebService<br /> @XmlSeeAlso({CustomTariffCost.class})<br /> public abstract interface CustomTariffCostService {<br /> <br /> @WebMethod<br /> public abstract List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id)<br /> throws BGException;<br /> <br /> @WebMethod<br /> public abstract void updateCustomTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs)<br /> throws BGException;<br /> <br /> /**<br /> * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> */<br /> @WebMethod<br /> public abstract Set&lt;Integer&gt; availableServiceList(@WebParam(name=&quot;tariffPlanId&quot;) int tpid) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;<br /> import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;<br /> import ru.bitel.bgbilling.server.util.ClosedDateChecker;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.List;<br /> import java.util.Set;<br /> import java.util.TreeSet;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * Реализация веб-сервиса для работы с персональными ценам<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService&quot;)<br /> public class CustomTariffCostServiceImpl extends AbstractService<br /> implements CustomTariffCostService {<br /> <br /> @Override<br /> public List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name = &quot;contractTariffId&quot;) int contract_tariff_id) throws BGException {<br /> return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);<br /> }<br /> <br /> @Override<br /> public void updateCustomTariffCostList(@WebParam(name = &quot;contractTariffId&quot;)int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs) throws BGException{<br /> <br /> Calendar closedDate;<br /> ContractTariffManager ctm = new ContractTariffManager(getConnection());<br /> ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);<br /> if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking(&quot;updateCustomTariffCostList&quot;, 0, this.userId)) != null)<br /> {<br /> ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());<br /> }<br /> CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());<br /> try {<br /> ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);<br /> } catch (SQLException e) {<br /> throw new BGException(e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> * На данный момент реализовано через метки - ищем все метки тарифного плана вида &quot;id - Название&quot;,<br /> * где id - это id услуги. Например &quot;5 - Интернет&quot;.<br /> */<br /> @Override<br /> public Set&lt;Integer&gt; availableServiceList(@WebParam(name = &quot;tariffPlanId&quot;) int tpid)<br /> throws BGException<br /> {<br /> Set&lt;Integer&gt; result = new TreeSet&lt;Integer&gt;();<br /> TariffLabelManager tlm = new TariffLabelManager(getConnection());<br /> //Получаем список id меток тарифа<br /> Set&lt;Integer&gt; tariffLabelIds = tlm.getTariffLabelIds(tpid);<br /> if(tariffLabelIds.size()&gt;0){<br /> //Берём список всех меток с названиями<br /> List&lt;TariffLabelItem&gt; tariffLabelItemList = tlm.getTariffLabelItemList();<br /> //Ищем среди меток тарифа метки с названиями вида &quot;id - Название&quot;<br /> /*for(Integer label_id : tariffLabelIds){<br /> TariffLabelItem label = tariffLabelItemList.get()<br /> }*/<br /> Pattern pattern = Pattern.compile(&quot;^(\\d+) - .+$&quot;);<br /> <br /> Matcher matcher;<br /> //Перебираем все метки из справочника<br /> for(TariffLabelItem label : tariffLabelItemList){<br /> //Если метка есть на нашем тарифе ...<br /> if(tariffLabelIds.contains(label.getId())){<br /> //...то смотрим её название<br /> matcher = pattern.matcher(label.getTitle());<br /> if(matcher.find()){//определился sid услуги - отлично, добавляем в результат<br /> try{<br /> result.add(Integer.valueOf(matcher.group(1)));<br /> }catch (Exception e){}<br /> }<br /> }<br /> }<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> ==== Клиетский интерфейс ====<br /> <br /> Для начала немного костылей.<br /> <br /> Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:<br /> <br /> [[Файл:custom_cost_labels.jpg]]<br /> <br /> === Отображение на договоре ===<br /> <br /> == Заключение ==</div> Mon, 08 May 2017 06:47:31 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Редактирование цен */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> <br /> Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.<br /> <br /> ==== Веб-сервис переопределения цен ====<br /> <br /> Вспомогательные классы:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import ru.bitel.common.model.Id;<br /> <br /> import java.io.Serializable;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Тарифный план договора для использования в веб-сервисе.<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя<br /> *<br /> */<br /> public class ContractTariff extends Id implements Serializable {<br /> private int contractId;<br /> private int tariffPlanId;<br /> private Date date1;<br /> private Date date2;<br /> private String comment = &quot;&quot;;<br /> private String tariffName;<br /> private int pos;<br /> private int entityMid;<br /> private int entityId;<br /> private int replacedFromContractTariffId;<br /> <br /> public ContractTariff() {<br /> this.id = -1;<br /> this.contractId = -1;<br /> this.tariffPlanId = -1;<br /> this.date1 = null;<br /> this.date2 = null;<br /> this.tariffName = null;<br /> this.pos = 0;<br /> this.entityId = 0;<br /> this.entityMid = 0;<br /> this.replacedFromContractTariffId = -1;<br /> }<br /> <br /> public int getContractId() {<br /> return contractId;<br /> }<br /> <br /> public void setContractId(int contractId) {<br /> this.contractId = contractId;<br /> }<br /> <br /> public int getTariffPlanId() {<br /> return tariffPlanId;<br /> }<br /> <br /> public String getTariffName() {<br /> return tariffName;<br /> }<br /> <br /> public void setTariffName(String tariffName) {<br /> this.tariffName = tariffName;<br /> }<br /> <br /> public void setTariffPlanId(int tariffPlanId) {<br /> this.tariffPlanId = tariffPlanId;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> <br /> public String getComment() {<br /> return comment;<br /> }<br /> <br /> public void setComment(String comment) {<br /> this.comment = comment;<br /> }<br /> <br /> public int getPos() {<br /> return pos;<br /> }<br /> <br /> public void setPos(int pos) {<br /> this.pos = pos;<br /> }<br /> <br /> public int getEntityMid() {<br /> return entityMid;<br /> }<br /> <br /> public void setEntityMid(int entityMid) {<br /> this.entityMid = entityMid;<br /> }<br /> <br /> public int getEntityId() {<br /> return entityId;<br /> }<br /> <br /> public void setEntityId(int entityId) {<br /> this.entityId = entityId;<br /> }<br /> <br /> public int getReplacedFromContractTariffId() {<br /> return replacedFromContractTariffId;<br /> }<br /> <br /> public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {<br /> this.replacedFromContractTariffId = replacedFromContractTariffId;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом<br /> *<br /> */<br /> @WebService<br /> @XmlSeeAlso({ContractTariff.class})<br /> public abstract interface ContractTariffService {<br /> @WebMethod<br /> public abstract List&lt;ContractTariff&gt; contractTariffList(@WebParam(name=&quot;contractId&quot;) int contracId) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;<br /> import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.util.List;<br /> <br /> /**<br /> * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService&quot;)<br /> public class ContractTariffServiceImpl extends AbstractService<br /> implements ContractTariffService {<br /> <br /> @Override<br /> public List&lt;ContractTariff&gt; contractTariffList(@WebParam(name = &quot;contractId&quot;) int contracId) throws BGException {<br /> return (new ContractTariffDao(getConnection()).list(contracId));<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Веб-сервис работы с переопределёнными ценами:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.common.service;<br /> <br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.jws.WebMethod;<br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import javax.xml.bind.annotation.XmlSeeAlso;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Интерфейс веб-сервиса для работы с персональными ценами<br /> */<br /> @WebService<br /> @XmlSeeAlso({CustomTariffCost.class})<br /> public abstract interface CustomTariffCostService {<br /> <br /> @WebMethod<br /> public abstract List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id)<br /> throws BGException;<br /> <br /> @WebMethod<br /> public abstract void updateCustomTariffCostList(@WebParam(name=&quot;contractTariffId&quot;) int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs)<br /> throws BGException;<br /> <br /> /**<br /> * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> */<br /> @WebMethod<br /> public abstract Set&lt;Integer&gt; availableServiceList(@WebParam(name=&quot;tariffPlanId&quot;) int tpid) throws BGException;<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.kernel.discount.api.server.service;<br /> <br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;<br /> import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;<br /> import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;<br /> import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;<br /> import ru.bitel.bgbilling.server.util.ClosedDateChecker;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;<br /> <br /> import javax.jws.WebParam;<br /> import javax.jws.WebService;<br /> import java.sql.SQLException;<br /> import java.util.Calendar;<br /> import java.util.List;<br /> import java.util.Set;<br /> import java.util.TreeSet;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * Реализация веб-сервиса для работы с персональными ценам<br /> */<br /> @WebService(endpointInterface=&quot;ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService&quot;)<br /> public class CustomTariffCostServiceImpl extends AbstractService<br /> implements CustomTariffCostService {<br /> <br /> @Override<br /> public List&lt;CustomTariffCost&gt; customTariffCostList(@WebParam(name = &quot;contractTariffId&quot;) int contract_tariff_id) throws BGException {<br /> return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);<br /> }<br /> <br /> @Override<br /> public void updateCustomTariffCostList(@WebParam(name = &quot;contractTariffId&quot;)int contract_tariff_id, @WebParam(name=&quot;costs&quot;) List&lt;CustomTariffCost&gt; costs) throws BGException{<br /> <br /> Calendar closedDate;<br /> ContractTariffManager ctm = new ContractTariffManager(getConnection());<br /> ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);<br /> if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking(&quot;updateCustomTariffCostList&quot;, 0, this.userId)) != null)<br /> {<br /> ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());<br /> }<br /> CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());<br /> try {<br /> ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);<br /> } catch (SQLException e) {<br /> throw new BGException(e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену<br /> * На данный момент реализовано через метки - ищем все метки тарифного плана вида &quot;id - Название&quot;,<br /> * где id - это id услуги. Например &quot;5 - Интернет&quot;.<br /> */<br /> @Override<br /> public Set&lt;Integer&gt; availableServiceList(@WebParam(name = &quot;tariffPlanId&quot;) int tpid)<br /> throws BGException<br /> {<br /> Set&lt;Integer&gt; result = new TreeSet&lt;Integer&gt;();<br /> TariffLabelManager tlm = new TariffLabelManager(getConnection());<br /> //Получаем список id меток тарифа<br /> Set&lt;Integer&gt; tariffLabelIds = tlm.getTariffLabelIds(tpid);<br /> if(tariffLabelIds.size()&gt;0){<br /> //Берём список всех меток с названиями<br /> List&lt;TariffLabelItem&gt; tariffLabelItemList = tlm.getTariffLabelItemList();<br /> //Ищем среди меток тарифа метки с названиями вида &quot;id - Название&quot;<br /> /*for(Integer label_id : tariffLabelIds){<br /> TariffLabelItem label = tariffLabelItemList.get()<br /> }*/<br /> Pattern pattern = Pattern.compile(&quot;^(\\d+) - .+$&quot;);<br /> <br /> Matcher matcher;<br /> //Перебираем все метки из справочника<br /> for(TariffLabelItem label : tariffLabelItemList){<br /> //Если метка есть на нашем тарифе ...<br /> if(tariffLabelIds.contains(label.getId())){<br /> //...то смотрим её название<br /> matcher = pattern.matcher(label.getTitle());<br /> if(matcher.find()){//определился sid услуги - отлично, добавляем в результат<br /> try{<br /> result.add(Integer.valueOf(matcher.group(1)));<br /> }catch (Exception e){}<br /> }<br /> }<br /> }<br /> }<br /> return result;<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> ==== Клиетский интерфейс ====<br /> <br /> === Отображение на договоре ===<br /> <br /> == Заключение ==</div> Mon, 08 May 2017 06:43:08 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.<br /> <br /> == Интерфейс ==<br /> <br /> === Редактирование цен ===<br /> === Отображение на договоре ===<br /> <br /> == Заключение ==</div> Mon, 08 May 2017 06:29:11 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Альтернативный узел &quot;Стоимость&quot; модуля NPay */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.<br /> <br /> Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.<br /> <br /> Все их нужно переопределить по-отдельности.<br /> <br /> Дневной:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class DayModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt;{<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int costType;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.costType = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> /*List&lt;TariffTreeNodeHolder&gt; path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));<br /> for (TariffTreeNodeHolder node : path){<br /> logger.info(String.valueOf(treeNodeId)+&quot; : &quot;+String.valueOf(node.treeNodeId));<br /> }*/<br /> <br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));<br /> <br /> req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Месячный:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> private final int type;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.type = parameterMap.getInt(&quot;type&quot;, 0);<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> @Override<br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> int monthDays = req.getAccountingMonthDays();<br /> int periodDays = req.serviceCost.accountingPeriodDays;<br /> <br /> BigDecimal monthAmount = req.serviceCost.monthAmount;<br /> BigDecimal periodAmount = req.serviceCost.periodAmount;<br /> BigDecimal cost;<br /> switch (this.type)<br /> {<br /> case 0:<br /> if (periodDays &gt; 0)<br /> {<br /> cost = this.currentCost;<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 1:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(<br /> BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> break;<br /> case 2:<br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> }<br /> else<br /> {<br /> cost = BigDecimal.ZERO;<br /> }<br /> break;<br /> case 3:<br /> cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);<br /> <br /> if ((monthAmount != null) &amp;&amp; (periodAmount != null))<br /> {<br /> BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);<br /> if (costPropAmount.compareTo(cost) &gt; 0)<br /> {<br /> cost = costPropAmount;<br /> }<br /> }<br /> <br /> break;<br /> default:<br /> cost = BigDecimal.ZERO;<br /> }<br /> <br /> req.serviceCost.cost = cost;<br /> <br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Годовой:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.bitel.bgbilling.modules.npay.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;<br /> <br /> import java.math.BigDecimal;<br /> <br /> /**<br /> * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены<br /> * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache<br /> */<br /> public class YearModeCustomCostTariffTreeNode extends TariffTreeNode&lt;NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext&gt; {<br /> /**<br /> * cost, указанный в тарифе<br /> */<br /> private final BigDecimal cost;<br /> //Может ли быть переопределена цена персонально в договоре<br /> private final boolean isCustomCostAllowed;<br /> /**<br /> * cost на конкретном договоре, используемый для рассчетов<br /> */<br /> private BigDecimal currentCost;<br /> <br /> public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)<br /> {<br /> super(id, parameterMap);<br /> <br /> this.cost = parameterMap.getBigDecimal(&quot;cost&quot;, BigDecimal.ZERO);<br /> this.currentCost = this.cost;<br /> this.isCustomCostAllowed = parameterMap.getBoolean(&quot;custom&quot;, false);<br /> }<br /> <br /> protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {<br /> this.currentCost = this.cost;<br /> //Определяем, нужно ли менять цену<br /> if(this.isCustomCostAllowed){<br /> //Ищем на договоре переопределение цены для услуги<br /> try{<br /> CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());<br /> if(customTariffCost!=null){<br /> this.currentCost=customTariffCost.getValue();<br /> logger.debug(&quot;custom cost for node_id=&quot;+treeNodeId+&quot;, cid=&quot;+req.cid+&quot;, sid=&quot;+req.serviceCost.serviceId+&quot;, value=&quot;+this.currentCost+&quot; (was &quot;+this.cost+&quot;)&quot;);<br /> }<br /> }catch (Exception e){<br /> logger.error(e.getMessage(), e);<br /> }<br /> }<br /> <br /> req.serviceCost.cost = this.currentCost;<br /> return 1;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Теперь заменим стандартные узлы тарифов абонплат нашими самописными.<br /> Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode&quot;/&gt;<br /> ...<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode&quot;/&gt; <br /> ... <br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode&quot;/&gt; <br /> &lt;/source&gt;<br /> <br /> На наши:<br /> <br /> &lt;source lang=xml&gt;<br /> &lt;node type=&quot;day_cost&quot; class1=&quot;bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;month_cost&quot; class1=&quot;bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;node type=&quot;year_cost&quot; class1=&quot;bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode&quot; title=&quot;Стоимость&quot; class2=&quot;ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode&quot;/&gt;<br /> &lt;/source&gt;<br /> <br /> Как это сделать?<br /> <br /> С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении.<br /> С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar<br /> <br /> Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.</div> Mon, 08 May 2017 06:26:28 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Кэш таблицы переопределений цен в памяти для ускорения тарификации */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений:</div> Mon, 08 May 2017 06:09:16 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> !!! Внимание! пп. 2 и 3 являются &quot;хаками&quot; стандартного поведения биллинга: требуется подмена стандартных классов своими !!!<br /> <br /> Скриншоты:<br /> <br /> Интерфейс переопределения цен:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> Отображение на договоре:<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> В тарифном дереве:<br /> <br /> [[Файл:custom_cost2.jpg]]<br /> <br /> = Реализация =<br /> <br /> == Переопределение цены ==<br /> <br /> === Таблица для хранения переопределений цен ===<br /> <br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_tariff_cost` (<br /> `sid` int(10) unsigned NOT NULL,<br /> `value` decimal(15,5) NOT NULL,<br /> `contract_tariff_id` int(10) unsigned NOT NULL,<br /> PRIMARY KEY (`sid`,`contract_tariff_id`),<br /> KEY `sid` (`sid`)<br /> &lt;/source&gt;<br /> <br /> Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id-&gt;contract_tariff.id).<br /> <br /> Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.<br /> <br /> Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.<br /> <br /> === Кэш таблицы переопределений цен в памяти для ускорения тарификации ===<br /> <br /> Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object|DAO] для работы с таблицей.<br /> <br /> Объект:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.api.common.bean;<br /> <br /> import java.io.Serializable;<br /> import java.math.BigDecimal;<br /> import java.util.Date;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> */<br /> public class CustomTariffCost implements Serializable {<br /> private int contract_tariff_id;<br /> private int sid;<br /> private BigDecimal value;<br /> private Date date1;<br /> private Date date2;<br /> <br /> public CustomTariffCost(){<br /> this.contract_tariff_id = -1;<br /> this.sid = -1;<br /> this.value = null;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> @Override<br /> public String toString() {<br /> StringBuilder sb = new StringBuilder(&quot;[&quot;);<br /> sb.append(&quot;contract_tariff_id = &quot;)<br /> .append(this.contract_tariff_id)<br /> .append(&quot;, sid = &quot;)<br /> .append(this.sid)<br /> .append(&quot;, value = &quot;)<br /> .append(this.value)<br /> .append(&quot;]&quot;);<br /> return sb.toString();<br /> }<br /> <br /> //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {<br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {<br /> //this.cid = cid;<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = date1;<br /> this.date2 = date2;<br /> }<br /> <br /> public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){<br /> this.contract_tariff_id = contract_tariff_id;<br /> this.sid = sid;<br /> this.value = value;<br /> this.date1 = null;<br /> this.date2 = null;<br /> }<br /> <br /> public int getContract_tariff_id() {<br /> return contract_tariff_id;<br /> }<br /> <br /> public void setContract_tariff_id(int contract_tariff_id) {<br /> this.contract_tariff_id = contract_tariff_id;<br /> }<br /> <br /> public int getSid() {<br /> return sid;<br /> }<br /> <br /> public void setSid(int sid) {<br /> this.sid = sid;<br /> }<br /> <br /> public BigDecimal getValue() {<br /> return value;<br /> }<br /> <br /> public void setValue(BigDecimal value) {<br /> this.value = value;<br /> }<br /> <br /> public Date getDate1() {<br /> return date1;<br /> }<br /> <br /> public void setDate1(Date date1) {<br /> this.date1 = date1;<br /> }<br /> <br /> public Date getDate2() {<br /> return date2;<br /> }<br /> <br /> public void setDate2(Date date2) {<br /> this.date2 = date2;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> События изменения (для кэша):<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на договоре<br /> */<br /> @XmlRootElement<br /> public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> <br /> public ContractCustomTariffCostsChangedEvent(int cid, int userId) {<br /> super(0, cid, userId);<br /> }<br /> <br /> protected ContractCustomTariffCostsChangedEvent(){<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean.event;<br /> <br /> import ru.bitel.bgbilling.kernel.event.Event;<br /> import ru.bitel.common.SerialUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> <br /> import javax.xml.bind.annotation.XmlRootElement;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Событие при изменении персональной цены на тарифе договора<br /> */<br /> @XmlRootElement<br /> public class CustomTariffCostChangedEvent extends Event//QueueEvent<br /> {<br /> private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);<br /> private CustomTariffCost cost;<br /> <br /> public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {<br /> super(0, cid, userId);<br /> this.cost = cost;<br /> }<br /> <br /> public CustomTariffCost getCost(){<br /> return this.cost;<br /> }<br /> <br /> protected CustomTariffCostChangedEvent(){<br /> this.cost = null;<br /> }<br /> <br /> }<br /> <br /> &lt;/source&gt;<br /> <br /> DAO:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import bitel.billing.server.contract.bean.ContractTariff;<br /> import bitel.billing.server.contract.bean.ContractTariffManager;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.server.util.ServerUtils;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.List;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * Dao для работы с персональными ценами<br /> */<br /> public class CustomTariffCostDao{<br /> protected Connection con;<br /> <br /> private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);<br /> <br /> public CustomTariffCostDao(Connection con) {<br /> this.con = con;<br /> }<br /> <br /> private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;delete from custom_tariff_cost where contract_tariff_id=?&quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ps.executeUpdate();<br /> ps.close();<br /> }<br /> <br /> public void updateContractCustomTariffCosts(ContractTariff ct, List&lt;CustomTariffCost&gt; costs, int userId) throws SQLException, BGException{<br /> this.removeContractCustomTariffCosts(ct.getId());<br /> if(null!=costs){<br /> for(CustomTariffCost cost : costs){<br /> if(ct.getId()!=cost.getContract_tariff_id()){<br /> throw new BGException(&quot;Attempt to update custom tariff cost: &quot;+cost.toString()+&quot; for another contract tariff = &quot;+ct.getId());<br /> }<br /> this.update0(cost);<br /> }<br /> }<br /> <br /> EventProcessor.getInstance().publish(new<br /> ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));<br /> }<br /> <br /> public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {<br /> ContractTariffManager ctm = new ContractTariffManager(this.con);<br /> ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());<br /> cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));<br /> cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));<br /> update0(cost);<br /> <br /> EventProcessor.getInstance().publish(new<br /> CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));<br /> return cost;<br /> }<br /> <br /> private void update0(CustomTariffCost cost) throws SQLException, BGException{<br /> PreparedStatement ps = this.con.prepareStatement(<br /> &quot;insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) &quot; +<br /> &quot;on duplicate key update value=?&quot;);<br /> ps.setInt(1, cost.getContract_tariff_id());<br /> ps.setInt(2, cost.getSid());<br /> ps.setBigDecimal(3, cost.getValue());<br /> ps.setBigDecimal(4, cost.getValue());<br /> ps.executeUpdate();<br /> ps.close();<br /> ServerUtils.commitConnection(this.con);<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(ContractTariff ct) throws BGException {<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; contract_tariff_id, &quot; +<br /> &quot; sid, &quot; +<br /> &quot; value &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, ct.getContractId());<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertCalendarToDate(ct.getDate1()),<br /> TimeUtils.convertCalendarToDate(ct.getDate2())));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> public List&lt;CustomTariffCost&gt; getContractTariffCosts(int contract_tariff_id) throws BGException{<br /> List&lt;CustomTariffCost&gt; result = new ArrayList&lt;CustomTariffCost&gt;();<br /> try{<br /> PreparedStatement ps = this.con.prepareStatement(&quot;select &quot; +<br /> &quot; ctc.contract_tariff_id, &quot; +<br /> &quot; ctc.sid, &quot; +<br /> &quot; ctc.value,&quot; +<br /> &quot; ct.date1,&quot; +<br /> &quot; ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot; custom_tariff_cost ctc left join &quot; +<br /> &quot; contract_tariff ct on ctc.contract_tariff_id=ct.id &quot; +<br /> &quot;where &quot; +<br /> &quot; ctc.contract_tariff_id=? &quot;);<br /> ps.setInt(1, contract_tariff_id);<br /> ResultSet rs = ps.executeQuery();<br /> while(rs.next()){<br /> result.add(new CustomTariffCost(<br /> rs.getInt(1),<br /> rs.getInt(2),<br /> rs.getBigDecimal(3),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(4)),<br /> TimeUtils.convertSqlDateToDate(rs.getDate(5))));<br /> }<br /> rs.close();<br /> ps.close();<br /> }catch (SQLException e){<br /> processException(e);<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected void processException(SQLException e)<br /> throws BGException<br /> {<br /> throw new BGException(e.getMessage() + &quot; [&quot; + e.getSQLState() + &quot;, &quot; + e.getErrorCode() + &quot;]&quot;, e);<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> Сам кэш переопределений цен:<br /> <br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.kernel.discount.bean;<br /> <br /> import bitel.billing.common.TimeUtils;<br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;<br /> import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;<br /> <br /> import java.lang.ref.SoftReference;<br /> import java.math.BigDecimal;<br /> import java.sql.Connection;<br /> import java.sql.PreparedStatement;<br /> import java.sql.ResultSet;<br /> import java.sql.SQLException;<br /> import java.util.ArrayList;<br /> import java.util.Date;<br /> import java.util.List;<br /> import java.util.concurrent.ConcurrentHashMap;<br /> import java.util.concurrent.ConcurrentMap;<br /> import java.util.concurrent.atomic.AtomicReference;<br /> <br /> /**<br /> * Кэш переопределённых цен на договоре<br /> */<br /> public class CustomTariffCostCache {<br /> <br /> private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();<br /> private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);<br /> <br /> //cid -&gt; tree_id -&gt; sid -&gt; List&lt;ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost&gt;<br /> private final ConcurrentMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt; tariffCostMap =<br /> new ConcurrentHashMap&lt;Integer, SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;&gt;();<br /> <br /> private CustomTariffCostCache() {<br /> new Thread(&quot;custom-tariff-cache-reload&quot;)<br /> {<br /> public void run()<br /> {<br /> try<br /> {<br /> //При изменении переопределённых цен на тарифе обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;CustomTariffCostChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , CustomTariffCostChangedEvent.class);<br /> <br /> //При изменении переопределённых цен на договоре обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractCustomTariffCostsChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try {<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractCustomTariffCostsChangedEvent.class);<br /> <br /> <br /> //При изменении тарифа договора обновляем все переопределения для cid<br /> EventProcessor.getInstance().addListener(new EventListener&lt;ContractTariffChangedEvent&gt;()<br /> {<br /> @Override<br /> public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)<br /> throws BGException<br /> {<br /> try{<br /> CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());<br /> } catch (SQLException ex) {<br /> throw new BGException(ex);<br /> }<br /> }<br /> <br /> }<br /> , ContractTariffChangedEvent.class);<br /> }catch (BGException e){<br /> CustomTariffCostCache.logger.error(e.getMessage(), e);<br /> }<br /> }<br /> }<br /> .start();<br /> }<br /> <br /> public static CustomTariffCostCache getInstance(){<br /> return instance;<br /> }<br /> <br /> /**<br /> * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.<br /> * Если таких переопределений несколько - возвращаем первое.<br /> * (Предполагаем, что переопределение только одно в каждый момент времени)<br /> * @param cid cid<br /> * @param sid sid<br /> * @param tree_id id тарифного дерева<br /> * @param dt дата, на которую должно быть активно переопределение<br /> * @return объект переопределения цены<br /> */<br /> public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {<br /> <br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; cidCostMapRefRef =<br /> this.tariffCostMap.get(cid);<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; cidCostMapRef = null;<br /> if(cidCostMapRefRef!=null){<br /> cidCostMapRef = cidCostMapRefRef.get();<br /> }<br /> if(cidCostMapRef==null){//нет - значит нужно получить<br /> cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);<br /> }<br /> if(cidCostMapRef==null || cidCostMapRef.get()==null){<br /> //В кэше ничего нет для этого cid,<br /> // но есть пустой cidCostMapRef =&gt; переопределений цены на договоре нет<br /> //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)<br /> return null;<br /> }<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);<br /> if(cidTreeIdCostMap!=null){<br /> List&lt;CustomTariffCost&gt; costMapList = cidTreeIdCostMap.get(sid);<br /> if(costMapList!=null){<br /> //ищем в списке подходящее по датам переопределение<br /> for(CustomTariffCost cost : costMapList){<br /> if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&amp;&amp;<br /> (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){<br /> return cost;<br /> }<br /> }<br /> }<br /> <br /> }<br /> <br /> return null;<br /> }<br /> <br /> private AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {<br /> //Берём новую мэпу с костами для cid<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; cidCustomCosts = loadCidCustomCosts(con, cid);<br /> SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt; costMapRefRef = this.tariffCostMap.get(cid);<br /> if(costMapRefRef==null){<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }else{<br /> AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt; costMapRef = costMapRefRef.get();<br /> if(costMapRef!=null){<br /> costMapRef.set(cidCustomCosts);<br /> }else{<br /> costMapRefRef = new SoftReference&lt;AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;&gt;(new AtomicReference&lt;ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;&gt;(cidCustomCosts));<br /> this.tariffCostMap.put(cid,costMapRefRef);<br /> }<br /> }<br /> return costMapRefRef.get();<br /> }<br /> <br /> private ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; loadCidCustomCosts(Connection con, int cid) throws SQLException {<br /> ConcurrentMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt; result = null;<br /> PreparedStatement ps = con.prepareStatement(<br /> &quot;select &quot; +<br /> &quot;tp.tree_id, &quot; +<br /> &quot;ctc.sid, &quot; +<br /> &quot;ct.id, &quot; +<br /> &quot;ctc.value, &quot; +<br /> &quot;ct.date1, &quot; +<br /> &quot;ct.date2 &quot; +<br /> &quot;from &quot; +<br /> &quot;contract_tariff ct left join &quot; +<br /> &quot;custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join &quot; +<br /> &quot;tariff_plan tp on ct.tpid=tp.id &quot;+<br /> &quot;where &quot; +<br /> &quot;ct.cid=? AND &quot; +<br /> &quot;not ctc.contract_tariff_id is null AND &quot; +<br /> &quot;not tp.tree_id is null &quot;);<br /> ps.setInt(1,cid);<br /> ResultSet rs = ps.executeQuery();<br /> int tree_id;<br /> int sid;<br /> int contract_tariff_id;<br /> BigDecimal value;<br /> Date date1;<br /> Date date2;<br /> ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt; integerListConcurrentMap;<br /> List&lt;CustomTariffCost&gt; customTariffCosts;<br /> while(rs.next()){<br /> tree_id = rs.getInt(1);<br /> sid = rs.getInt(2);<br /> contract_tariff_id = rs.getInt(3);<br /> value = rs.getBigDecimal(4);<br /> date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));<br /> date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));<br /> <br /> if(null==result){<br /> result = new ConcurrentHashMap&lt;Integer, ConcurrentMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;&gt;();<br /> }<br /> integerListConcurrentMap = result.get(tree_id);<br /> if(null==integerListConcurrentMap){<br /> integerListConcurrentMap = new ConcurrentHashMap&lt;Integer, List&lt;CustomTariffCost&gt;&gt;();<br /> result.put(tree_id, integerListConcurrentMap);<br /> }<br /> customTariffCosts = integerListConcurrentMap.get(sid);<br /> if(null==customTariffCosts){<br /> customTariffCosts = new ArrayList&lt;CustomTariffCost&gt;();<br /> integerListConcurrentMap.put(sid, customTariffCosts);<br /> }<br /> customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));<br /> }<br /> rs.close();<br /> ps.close();<br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> === Альтернативный узел &quot;Стоимость&quot; модуля NPay ===<br /> <br /> Берём стандартный узел &quot;Стоимость&quot; и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений:</div> Mon, 08 May 2017 06:06:28 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Персональные цены для договоров http://wiki.bitel.ru/index.php/%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 <p>Cromeshnic:&#32;Новая страница: «{{Актуальность Версии|версия=5.2}} = Введение = Очень часто, особенно при работе с юр лицами, …»</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Введение =<br /> <br /> Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов.<br /> В текущей реализации биллинга это можно сделать только через персональные тарифы.<br /> Но персональные тарифы очень неудобны:<br /> * Ломают отчёты по тарифам<br /> * Осложняют внесение изменений в глобальные тарифы, от которых они наследованы<br /> * Требуют специальных знаний и прав доступа для корректного использования<br /> В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов<br /> <br /> = Описание решения =<br /> Тезисно решение выглядит следующим образом<br /> &lt;ol&gt;<br /> &lt;li&gt;Храним персональные цены на услуги в собственной таблице '''custom_tariff_cost''': contract_tariff_id, sid -&gt; value&lt;/li&gt;<br /> &lt;li&gt;Меняем стандартную логику тарифного узла &quot;стоимость&quot; модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены&lt;/li&gt;<br /> &lt;li&gt;Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами&lt;/li&gt;<br /> &lt;li&gt;Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> Скриншоты:<br /> <br /> [[Файл:custom_cost.jpg]]<br /> <br /> [[Файл:custom_cost1.jpg]]<br /> <br /> [[Файл:custom_cost2.jpg]]</div> Mon, 08 May 2017 05:19:08 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%9F%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D1%86%D0%B5%D0%BD%D1%8B_%D0%B4%D0%BB%D1%8F_%D0%B4%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80%D0%BE%D0%B2 Файл:Custom cost2.jpg http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost2.jpg <p>Cromeshnic:&#32;</p> <hr /> <div></div> Mon, 08 May 2017 05:18:51 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%84%D0%B0%D0%B9%D0%BB%D0%B0:Custom_cost2.jpg Файл:Custom cost1.jpg http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost1.jpg <p>Cromeshnic:&#32;</p> <hr /> <div></div> Mon, 08 May 2017 05:18:34 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%84%D0%B0%D0%B9%D0%BB%D0%B0:Custom_cost1.jpg Файл:Custom cost.jpg http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_cost.jpg <p>Cromeshnic:&#32;</p> <hr /> <div></div> Mon, 08 May 2017 05:17:59 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%84%D0%B0%D0%B9%D0%BB%D0%B0:Custom_cost.jpg Заглавная страница http://wiki.bitel.ru/index.php/%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0 <p>Cromeshnic:&#32;/* Модуль NPay */</p> <hr /> <div>== О BiTel Wiki ==<br /> Здесь вы можете получить больше информации о продуктах BiTel: BGBilling, BGCRM, а также поделиться своим опытом с другими пользователями. В то время как документация часто предоставляет общие сведения о системе и ее настройках, в WiKi приводятся конкретные примеры.<br /> <br /> * &lt;b&gt;[[Как выложить статью на WiKi]]&lt;/b&gt;<br /> * [[Оформление статей]]<br /> * [http://forum.bitel.ru/ Форум BiTel]<br /> <br /> == Специалисты ==<br /> Уважаемые &quot;продвинутые пользователи&quot;. Здесь вы можете располагать записи со своими контактами для оказания воздмездной или безвозмездной помощи по настройке системы BGBilling пользователям, не столь далеко продвинувшимся. Желательно указывать ваши контактные данные и &quot;специализацию&quot;. Отзывы по исполнителям можно оставить/почитать [http://forum.bitel.ru/viewtopic.php?t=9329 на форуме].<br /> {| border=&quot;1&quot; cellpadding=&quot;2&quot; cellspacing=&quot;0&quot;<br /> |- valign=top align=&quot;center&quot; bgcolor=&quot;#eeeeee&quot; <br /> | Имя || Специализация || Контакт || Примечания<br /> |-<br /> | Олег Алтынников || Установка, настройка, поддержка биллинга, миграция с других|| E-mail: murano@linkray.ru, ICQ: 462851472 || Написание скриптов, интеграция с любым оборудованием (IPTV, Интернет, DPI, Телефония), проекты любой сложности под любые задачи, автоматизация, оптимизация работы, Linux/FreeBSD и многое другое<br /> |-<br /> |-<br /> | Рустам Тазуркаев || Mikrotik, переход с NetUp, CISCO || [[Изображение:Cpec_2_contact.png]] &lt;!-- ICQ: 648986--&gt; ||<br /> |-<br /> | Михаил Чернобаев || Скрипты биллинга, Java-расширения, интеграция с другими системами || Тел. +79619322588 || Автоматизация бизнес-процессов: скриптование в биллинге, расширение BGBS API, разработка инструментов интеграции с другими системами. Переход на BGBilling. Возможны крупные проекты.<br /> |- <br /> | Борис Близнюков || Скрипты биллинга, CISCO, Voip, Mera || [[Изображение:Cpec_4_contact.png]] &lt;!--ICQ: 1996944--&gt; || Только бесплатные краткие консультации. Очень хороший специалист по CISCO.<br /> |-<br /> | Андрей Бехтерев || Cisco, UNIX, ISP, Asterisk || ICQ: 7021464 WEB: http://behterev.su/ || Обширный спектр оборудования. Консалтинг.<br /> |-<br /> | Гершевич М.М. || Доработка конфигурации 1С и прочего ПО. || Тел. +79248454888 +7-(4162)-238-777 WEB: http://www.amurimpulse.ru/ mail: mike1008@mail.ru || Консалтинг. Информационная безопасность. Интеграция биллинга. Крупные проекты. Работа под заказ.<br /> |-<br /> | Андрей Зюзенков || Android, Linux, Eltex, Asterisk, BGBilling || [https://bghelp.ru Сайт], e-mail: info@bghelp.ru || Интеграция, мобильные приложения на Android<br /> |-<br /> | Семён Кошечкин || Java || email/gtalk: [[Файл:Cpec_5_contact.jpg]]|| Скрипты, дополнения, модуль Inet. [[%D0%A1%D0%BB%D1%83%D0%B6%D0%B5%D0%B1%D0%BD%D0%B0%D1%8F:Contributions/Cromeshnic|wiki]]<br /> |-<br /> |-<br /> | Конференция BGBilling || вопросы касаемо системы BGBilling || https://telegram.me/joinchat/A3Nz5QUFAbB_o0HKOdNYRw || Администраторы АСР BGBillig(иногда и разработчики) помогают друг другу в разных вопросах.<br /> |}<br /> <br /> == BGBilling ==<br /> === Установка ===<br /> * [[Установка на gentoo]]<br /> * [[Установка на Sun Solaris]]<br /> * [[Установка на Slackware]]<br /> * [[Установка на FreeBSD]]<br /> * [[Установка на Ubuntu 8 Desktop]]<br /> * [[Установка на Ubuntu 9.10 Desktop]]<br /> <br /> <br /> ==== Перенос данных в биллинг ====<br /> *[[Пример конвертера данных из csv-файлов в базу BGBilling]]<br /> *[[Пример конвертера данных из CSV-файлов в базу BGBilling - 2]]<br /> *[[Пример конвертера данных из CSV-файлов в базу BGBilling - 3]]<br /> *[[Конвертер базы Netup]]<br /> *[[Примеры конвертеров данных из других биллинговых систем]]<br /> <br /> === Администрирование ===<br /> * [[Разграничение прав действий]]<br /> * [[Настройка безопасности сервера биллинга и компонентов биллинга]]<br /> * [[Принудительный останов процессов биллинга]]<br /> * [[Использование подписанного SSL сертификата]]<br /> * [[Запуск scheduler и data_loader с другими портами управления]]<br /> * [[bg-snmp-management|Мониторинг java-процессов по snmp]]<br /> * [[Пример юнита для systemd]]<br /> * [[Скрипты автостарта демонов bgbilling для Debian]]<br /> * [[javaws|Запуск BGBillingClient через Java Web Start]]<br /> * [[Мониторинг Inet-Radius через JMX]]<br /> * [[Интеграция существующего сертификата и приватного ключа SSL в хранилище keystore]]<br /> <br /> === Настройка вспомогательного ПО ===<br /> *[[Проксирование обращений к BGBillingServer посредством nginx]]<br /> <br /> ==== WildFly личный кабинет ====<br /> *[[Включение https]]<br /> <br /> ==== MySQL ====<br /> *[[Рекомендации по настройке MySQL]]<br /> *[[database backup|Backup MySQL базы с помощью snapshot'ов (Linux, LVM)]]<br /> *[[Настройка MySQL репликации]]<br /> *[[Установка триггера в MySQL для отслеживания изменений]]<br /> *[[Скрипт восстановления MySQL репликации]]<br /> *[[Simple DB backup]]<br /> *[[Galera]]<br /> <br /> ==== NetFlow ====<br /> *[[Настройка NetFlow-агента IPCAD]]<br /> *[[Разделение NetFlow-потоков]]<br /> <br /> === Технологии ===<br /> *[[BGBilling_XSLT|XSLT]]<br /> <br /> === Разработка ===<br /> *[[Отладка action'ов в IntelliJ IDEA]]<br /> *[[Разработка динамического кода в IDE Eclipse]]<br /> <br /> === [[XSLT]] шаблоны ===<br /> *[[Добавление параметров договора на страницу личного кабинета]]<br /> &lt;!-- *[[Красивые графики статистики в модуле IPN]] --&gt;<br /> *[[Карточки договора]]<br /> *[[Создание XSLT/FO шаблона со штрихкодами]]<br /> *[[Подстановка данных в зависимости от текущего пользователя биллинга]]<br /> *[[Генерация прайса модуля IP телефонии в карточке договора]]<br /> <br /> ==== Счета ====<br /> *[[Печать счета-фактуры и акта на отдельных листах]]<br /> *[[Расширенные счета модуля бухгалтерии]]<br /> *[[Квитанция телефонии физ. лицам]]<br /> *[[Шаблоны вывода названия месяца]]<br /> *[[Изменения в шаблоне в зависимости от месяца документа]]<br /> *[[Добавление новых шрифтов в FO шаблоны]]<br /> <br /> === Интеграция с внешними системами ===<br /> * [[Прямая интеграция с платежными терминалами ЭСФОР / SFOUR]]<br /> * [[Интеграция с платежной системой с использованием модуля Card]]<br /> * [[Интеграция с платежной системой Robokassa]]<br /> * [[SMS рассылка через SMPP]]<br /> * [[SMS рассылка через SMPP по средствам дин кода в 5.2]]<br /> * [[Система учета &quot;Заявки и Наряды&quot; на java]]<br /> * [[Bash скрипт-отсылка смс через телефон при отсутствие ping на заданный узел]]<br /> * [[Запросы в личный кабинет пользователя сторонними системами]]<br /> * [[Запросы к серверу биллинга сторонними системами]]<br /> <br /> ==== 1С ====<br /> * [[BGBilling-1C]]<br /> * [[amurimpulse.ru bgbilling]]<br /> * [[Integrator 1C-BGBilling]]<br /> * [[Пример обращения к биллингу из 1С v.7.7]]<br /> * [[Пример интеграции с 1С v.7.7]]<br /> * [[Пример интеграции с 1С v.8.1]]<br /> * [[Пример интеграции с 1С через custom API]]<br /> * [[Установка unload_status счета через HTTP-запрос]]<br /> ==== Android ====<br /> *[[Разработка мобильных приложений на Android]]<br /> <br /> === Динамический код (скрипты BGBS для старых версий) ===<br /> *[[Логгирование в скриптах поведения]]<br /> <br /> ==== Динамический код ====<br /> *[[Конвертирование адреса]]<br /> *[[Глобальная синхронзация услуг модуля npay с тарифным планом]]<br /> *[[Новый номер договора группе договоров]]<br /> *[[Скрипт проверки таймзон (timezone, tz, tzdata) в java]]<br /> <br /> ==== Комплексные решения ====<br /> *[[Предоставление тестового периода пользования услугой]]<br /> *[[Организация системы отслеживания и отключения КТВ должников на BGBS с использованием CRM плагина]]<br /> *[[Автоматизация подключений VPN-клиентов с использованием CRM плагина]]<br /> *[[Пример автоматизации подключения новых клиентов]]<br /> <br /> ==== Глобальные скрипты ====<br /> *[[Скрипт глобальный отмены перехода на тарифы при неоплате]]<br /> *[[Скрипт предоставление скидки пенсионерам]]<br /> *[[Скрипт создания субдоговоров по шаблону]]<br /> *[[Глобальное событие запуска сервера]]<br /> *[[Перемещение в группу через 3 месяца если не было движения денег в наработке]]<br /> *[[Поиск и изменение статусов у договоров]]<br /> *[[Получение списка доступных действий в SQL]]<br /> *[[Глобальный скрипт для удаления старых таблиц]]<br /> <br /> ==== Пользовательские библиотеки скриптов ====<br /> *[[Пересчеты и бонусы]]<br /> *[[Архивирование логов netflow и radius accaunting]]<br /> <br /> ==== Ядро ====<br /> *[[Смена тарифного плана по заданию пользователя]]<br /> *[[Валидация текстового параметра]]<br /> *[[Проверка ИНН/КПП при вводе]]<br /> *[[Проверка параметра договора перед изменением]]<br /> *[[Обработка смены параметра договора]]<br /> *[[Создание списка дополнительных действий для договора]]<br /> *[[Обработка события создания договора]]<br /> *[[Обработка события &quot;добавление услуги RSCM в договор&quot; . Скипт сменяет тариф, подключает абонплату ]]<br /> *[[Приостановление договора клиентом через WEB]]<br /> *[[Скрипт проверки баланса и отключения договора]]<br /> *[[Изменение стандартной логики перетирания статусов]]<br /> *[[Пример продажи OEM ключей с помощью скрипта]]<br /> *[[Пример копирования тарифного плана]]<br /> *[[Получение текущего пользователя биллинга]]<br /> *[[Запуск скрипта до и после акшена]]<br /> *[[Примеры скриптов до и после акшена]]<br /> *[[Примеры динамического кода акшена и веб-сервисов]]<br /> *[[Начисление бонусов на счет при платежах определенного типа]]<br /> *[[Включение должников по приходу платежа]]<br /> *[[Снижение лимита при внесении расхода]]<br /> *[[Изменение суммы лимита определенной группе договоров]]<br /> *[[Синхронизация услуг договора в соответствии с тарифными планами]]<br /> *[[Добавление группы и снятие в зависимости от статуса]]<br /> *[[Управление статусом договора по состоянию баланса]]<br /> *[[Запрет на вход в личный кабинет с закрытых договоров]]<br /> *[[Переход на понижающий тариф только со следующего месяца]]<br /> *[[Пример создания своего интерфейса в клиенте]]<br /> *[[Метки услуг]]<br /> *[[Сравнение прав пользователей]]<br /> *[[Свой список шаблонов договоров]]<br /> *[[Модификация приходящего платежа (снятие процента)]]<br /> *[[Подкрашивание договоров в поиске]]<br /> *[[Пример добавления пунктов в ЛК редиректящих на url]]<br /> <br /> ==== Модуль Bill ====<br /> *[[Создание счета в модуле Bill]]<br /> *[[Создание счета из суммы платежей по классу договоров]]<br /> *[[Создание счета по таблице позиций]]<br /> *[[Создание счета и счет-фактур в модуле Bill(выполнение тех же действий что и руками)]]<br /> *[[Создание счетов на предоплату]]<br /> *[[Распечатка счетов в pdf по событию генерации счета]]<br /> *[[Внешняя программа на JAVA для синхронизации номеров счетов и актов выполненных работ (версия BGBilling 5.0)]]<br /> *[[Автоматическая отправка счетов через глобальный скрипт поведения]]<br /> <br /> ==== Модуль DialUp ====<br /> *[[Запуск переначисления в модуле DialUp]]<br /> *[[Передача ACCEPT вместо REJECT вместе с доп. аттрибутами]]<br /> *[[Обработка запроса учетного периода]]<br /> *[[Переинициализация тарифа в пределах сессии | Обработка запроса учетного периода (переинициализация тарифа в пределах сессии) ]]<br /> *[[Ограничение доступа для различных групп пользователей для BGRadiusDialup]]<br /> *[[Детальное информирование абонентов о причинах ошибки 691]]<br /> *[[Аутентификация с учетом Calling-Id-Station]]<br /> *[[Доп. действие сброса активных соединений]]<br /> *[[Открытие абонплаты по первой установке соединения]]<br /> *[[Пересчет трафика по данным Radius (при потерянных Netflow-логах)]]<br /> *[[Отключение Fake сессий при приходе платежа]]<br /> *[[Ограничение доступа на основе объектов]]<br /> <br /> ==== Модуль DialUp / Cкрипты предобработки RADIUS запросов ====<br /> * [[Уcтановка услуги типа &quot;Время&quot; для BGRadiusDialup]]<br /> * [[Установка фиксированного пароля]]<br /> * [[Нормализация параметра Acct-Session-Id у маршрутизатора Cisco]]<br /> * [[Разделение атрибута User-Name на логин и пароль]]<br /> * [[Вынос MAC адреса из cisco-avp-pair в Calling-Station-Id]]<br /> * [[Копирование Тunnel-Client-Endpoint/Tunnel-Server-Endpoint в Calling-Station-Id/Called-Station-Id]]<br /> * [[Замена radius-атрибутов при авторизации]]<br /> <br /> ==== Модуль Inet / Cкрипты предобработки RADIUS запросов ====<br /> * [[Вынос MAC адреса из cisco-avp-pair в Calling-Station-Id для модуля Inet]]<br /> <br /> ==== Модуль СerberСrypt ====<br /> *[[Изменение подписки карты через web (cerbercrypt)]]<br /> *[[Управление подписками через веб (cerbercrypt)]]<br /> *[[Дин.код для синхронизации pairing с внешнего cas]]<br /> *[[Скрипт активации/деактивации карты при добавлении/удалении]]<br /> <br /> ==== Модуль NPay ====<br /> *[[Определение размера абонентской платы]]<br /> *[[Запуск переначисления в модуле NPay]]<br /> *[[Дебетовые абонплаты. Снятие штрафа за разблокировку.]]<br /> *[[Снятие абонентской платы в дебитовых договорах]]<br /> *[[Предварительное уведомление о блокировке по дебетовым абонплатам]]<br /> *[[ Начисление абонплат по схеме 15-15 ]]<br /> *[[Персональные цены для договоров]]<br /> <br /> ==== Модуль Phone ====<br /> *[[При создании поинта модуля Phone добавление в него абонплат]]<br /> *[[Закрытие_телефонных_договоров]]<br /> <br /> ==== Модуль RSCM ====<br /> *[[Запуск переначисления в модуле RSCM]]<br /> *[[Перенос суммы расхода в наработку RSCM модуля]]<br /> <br /> ==== Модуль VoiceIp ====<br /> *[[Определение стоимости звонка VoiceIp]]<br /> <br /> ==== Модуль VoiceIp / Cкрипты предобработки RADIUS запросов ====<br /> * [[Идентификация Voip оператора по подсети (транзит)]]<br /> * [[Установка параметров звонка Voip]]<br /> * [[Установка фиксированного пароля]]<br /> * [[Разделение атрибута User-Name на логин и пароль]]<br /> * [[Замена radius-атрибутов при авторизации]]<br /> <br /> ==== Плагин CRM ====<br /> *[[Обработка выполненных задач в журнале задач]]<br /> *[[Обработка задач по событию ядра &quot;Поступление платежа&quot;, создание новой задачи и изменение существующей]]<br /> *[[Пример получения информации о задаче]]<br /> *[[Уведомления монтажников о новых активных задачах путем отправки SMS XML запросом]]<br /> <br /> ==== Плагин CashCheck ====<br /> *[[Чек: добавление позиции]]<br /> *[[Чек: завершение формирования]]<br /> *[[Примеры скриптов CashCheck]]<br /> <br /> ==== Плагин Documents ====<br /> *[[Создание копий документа на договорах]]<br /> <br /> ==== Модуль Inet ====<br /> *[[Скрипт активации учетного периода]]<br /> *[[Скрипт закрытия соединений]]<br /> <br /> === Решения для модулей и плагинов ===<br /> <br /> ==== Модуль DialUP ====<br /> *[[Настройка Lucent Ascend MAX6000 в качестве DialUP сервера]]<br /> *[[Настройка Dial-IN сервера FreeBSD PPPD]]<br /> *[[Настройка VPN сервера LINUX PPPD + POPTOP]]<br /> *[[Настройка шейпера в LINUX PPPD]]<br /> *[[Настройка VPN сервера FreeBSD MPD]]<br /> *[[Настройка PPPoE сервера на Cisco-роутере]]<br /> *[[Настройка PPPoE и/или РРТР (VPN) на Mikrotik]]<br /> *[[Проблема с прохождением update пакетов и сброса сессий в Debian и Ubuntu дистрибутивах]]<br /> *[[Настройка Dial-IN Windows RRAS сервера]]<br /> *[[VPN доступ с повременной тарификацией на базе FreeBSD MPD]]<br /> *[[Организация семейства UNLIMIT тарифов на базе FreeBSD MPD]]<br /> *[[Примеры тарифных планов VPN/DialUp]]<br /> *[[Отключение сессий по PoD на CISCO]]<br /> *[[Пример скрипта управления уровнями BGRadiusDialup]]<br /> *[[Настройка cisco с поддеркой ISG]]<br /> *[[Настройка BGBilling c поддеркой ISG]]<br /> *[[Настройка BGBilling с RedBack SmartEdge (PPPOE)]]<br /> <br /> ==== Модуль E-Mail ====<br /> *[[Почтовая система Exim + Cyrus + OpenLDAP на FreeBSD]]<br /> *[[Postfix/MySQL/BGBilling]]<br /> *[[Postfix+dovecot+ldap]]<br /> *[[Postfix+Mysql+Virtual domains]]<br /> <br /> ==== Модуль Inet ====<br /> *[[Inet FAQ]]<br /> *[[Схемы подключения]]<br /> *[[Расширения]]<br /> *[[Конвертеры из IPN в INET]]<br /> *[[Конвертер: логины Dialup в сервисы inet]]<br /> *[[WiFi-портал с активацией по sms ]]<br /> *[[WiFi-портал с оплатой картой через assist ]]<br /> <br /> ==== Модуль IPN ====<br /> *[[IP/VPN]]<br /> *[[Примеры тарифных планов IPN]]<br /> *[[Настройка BGIPNNetflowCollector]]<br /> *[[Методика определения причины отсутствия трафика в отчете договора]]<br /> *[[Связка с flow-tools]]<br /> *[[Экспорт Netflow-данных в формат Nfdump]]<br /> *[[Реалиазация шлюза на Cisco]]<br /> *[[Реализация шлюзов на BeanShell,примеры стандартных и других шлюзов]] (Manad, Cisco, Zyxel, Mikrotik)<br /> *[[Изменения в manad для работы с одним pipe на множество IP адресов]]<br /> *[[FreeBSD manad, понимающий изменения правил в тарифах]]<br /> *[[Табличный FreeBSD manad, понимающий изменения правил в тарифах]]<br /> *[[Пример реализации скриптового универсального шлюза]]<br /> *[[Конвертер привязок услуг dialup в привязки ipn]]<br /> *[[Реализация скрипта Manad]]<br /> *[[Настройка шлюза Mikrotik]]<br /> *[[Обновление номеров интерфейсов при замене роутера]]<br /> <br /> ==== Модуль Phone ====<br /> * [[Конвертация и загрузка тарифов Телефонии в биллинг]]<br /> * [[Примеры тарифных планов Телефонии]]<br /> * [[Примеры реализации конверторов логов]]<br /> * [[Генератор отчётности для Совинтел]]<br /> <br /> ==== Модуль Reports ====<br /> *[[Редактирование отчетов в iReport]]<br /> *[[Примеры отчётов]]<br /> *[[Использование отчётов для организации универсального поиска]]<br /> *[[Табличные отчёты с динамическими столбцами]]<br /> *[[Табличные отчёты в динамическом коде]]<br /> *[[Сохранение JasperReports-отчёта на сервере в pdf]]<br /> <br /> ==== Модуль TV ====<br /> *[[Активация/добавление модуля на договор через дополнительное действие]]<br /> <br /> ==== Модуль VoiceIP ====<br /> *[[Интеграция Asterisk и BGBilling (Accounting) посредством скрипта предобработки запросов Radius]]<br /> *[[Интеграция Asterisk и BGBilling (Accounting) посредством изменения программного кода Asterisk]]<br /> *[[Интеграция c MVTS]]<br /> *[[Интеграция c Cisco Call Manager Express (CME)]]<br /> *[[Карточная IVR система на базе Cisco]]<br /> *[[Примеры IVR скриптов для Cisco]]<br /> *[[Пример настройки Cisco AS5350]]<br /> *[[Продажа пакетов минут на направления]]<br /> <br /> ==== Плагин Dispatch ====<br /> *[[Импорт старой схемы рассылок баланса в Dispatch]]<br /> <br /> === SQL-запросы ===<br /> *[[Схема связки таблиц тарифов]]<br /> *[[Разные SQL-запросы]]<br /> *[[SQL-запрос: кто сколько платит на каждом тарифе]]<br /> *[[Получение цен тарифов]]<br /> *[[Работа с группами, битовые маски]]<br /> *[[наработка по абонентке и услугам за месяц]]<br /> <br /> ==== CerberCrypt ====<br /> *[[Модуль CerberCrypt: Разные SQL-запросы]]<br /> *[[Модуль CerberCrypt: Поиск битых SQL-связей]]<br /> <br /> === Веб-Интерфейс ===<br /> *[[Свой action в личном кабинете]]<br /> **[[WebAction_CustomSuspend]] - управление статусом договора (v5.0)<br /> *[[Изменение параметров договора из личного кабинета]]<br /> *[[Как убрать ненужные действия в web]]<br /> <br /> === Протоколы ===<br /> *[[Протокол дилерский платежей]]<br /> *[[Протоколы, поддержанные в модуле MPS]]<br /> *[[Протоколы, поддержанные в модуле Phone]]<br /> *[[Медиа: Enaza.zip]]<br /> *[[Медиа: Payonline.zip]]<br /> <br /> === FAQ ===<br /> * [[Не запускается служба под Windows (BGBillingServer, BGCashcheckServer итд)]]<br /> * [[Вопросы вместо русских букв]]<br /> * [[Что происходит с пользователями при рестарте сервера биллинга и BGRadiusDialup]]<br /> * [[Тарификация максимального трафика]]<br /> * [[Field ... doesn't have a default value ]]<br /> * [[Character set ‘cp1251' is not a compiled character set and is not specified in the ‘C:\mysql\\share\charsets\Index.xml’ file ]]<br /> * [[com.mysql.jdbc.exceptions.MySQLSyntaxErrorException: Unknown database 'bgbilling' ]]<br /> * [[Договор не отображается в поиске]]<br /> * [[PPPD проблема с сессиями больше 4ГБ]]<br /> * [[Меню личного кабинета]]<br /> * [[Java.lang.NoClassDefFoundError:_javax/xml/bind/DataBindingException|FreeBSD: Java.lang.NoClassDefFoundError: javax/xml/bind/DataBindingException]]<br /> * [[Manad: после некоторого количества договоров начинает передавать данные на биллинг неправильно ]]<br /> * [[Ошибка выполнения скиптов: Undefined argument:]]<br /> * [[Ошибка в логе &quot;Too many open files&quot;]]<br /> * [[Ошибка в логе &quot;java.lang.OutOfMemoryError : unable to create new native Thread&quot;]]<br /> * [[Ошибка в клиенте &quot;Action NOT FOUND!..&quot;]]<br /> * [[Inet FAQ]]<br /> * [[java.lang.NoSuchMethodError]]<br /> * [[Много таблиц npay_add_cost_detail и npay_detail]]<br /> * [[Тормозит клиент BG под Windows]]<br /> * [[Unable to load authentication plugin]]<br /> <br /> == BGCRM ==<br /> <br /> === Настройка вспомогательного ПО ===<br /> * [[Проксирование обращений к BGCRM посредством nginx]]<br /> <br /> === Плагин BGBilling ===<br /> * [[Синхронизация справочников адресов с BGBilling]]<br /> * [[Активация доверительного платежа в привязанном к процессу договоре биллинга]]<br /> * [[Импорт контрагентов из договоров, sql запрос]]<br /> <br /> === Плагин Document ===<br /> * [[Примеры шаблонов для генерации документов]]<br /> * [[Примеры шаблонов для генерации документов (устаревшее)]]<br /> * [[Пустой шаблон]]<br /> <br /> === Плагин Report ===<br /> * [[Примеры отчётов BGCRM]]<br /> <br /> === Комплексные решения ===<br /> *[[Организация отключения должников КТВ]]<br /> *[[Интеграция с Asterisk для обработки входящих звонков]]<br /> <br /> === Интеграция с внешними системами ===<br /> *[[Asterisk - пример обращения от АТС]]<br /> *[[ВКонтакте - пример интеграции]]<br /> <br /> === Примеры динамического кода ===<br /> *[[Проверка уникальности контрагента по ИНН]]<br /> *[[Проверка уникальности контрагента по паспортным данным]]<br /> *[[Переключение статуса процессов по наступлению момента времени]]<br /> *[[Повышение приоритета процессов]]<br /> *[[Проверка правки параметра процесса]]<br /> *[[Изменение описания процесса по правке параметра]]<br /> *[[Обработка событий процесса согласования]]<br /> *[[Генерация новостей исполнителям при изменении процессов]]<br /> *[[Уведомление на email]]<br /> <br /> === Примеры JEXL конфигураций, скриптов обработки событий ===<br /> *[[Простая обработка событий процесса]]<br /> <br /> == DBInfo ==<br /> * [[Описание программы]]<br /> * [[Установка и настройка программы]]<br /> * [[Исходный код программы]]<br /> <br /> == Разработка ПО ==<br /> В данном разделе собираются рекомендации по разработке ПО. Это накопленная годами и пополняемая база знания призвана упростить обучение в первую очередь разработчиков, работающих с применяемыми в BiTel технологиями: Java, Web (JS, HTML), СУБД MySQL, LINUX, GIT. И разрабатывающих схожие приложения: тиражируемые продукты для автоматизации процессов организаций. Всё предельно конкретно, поэтому большая часть примеров будет приведена на Java.<br /> Однако значительная часть описываемых проблем и принципов довольно фундоментальна и может быть полезна разработчиками в иных областях.<br /> <br /> === В общем ===<br /> * [[Разработка]]<br /> * [[Оптимизация]]<br /> * [[Логирование]]<br /> * [[Сборка и публикация проекта]]<br /> <br /> === Java разработка ===<br /> * [[Работа с git в Eclipse(EGit)]]<br /> * [[Выявление неисправностей приложений]]<br /> * [[Обращение к Web-сервису]]<br /> * [[Работа с SQL в Java]]<br /> * [[Встроенный Application сервер в приложении]]<br /> * [[Потоки в Java]]<br /> * [[Обработка ошибок]]<br /> <br /> ==== Полезные Java библиотеки ====<br /> {| border=&quot;1&quot; cellpadding=&quot;2&quot; cellspacing=&quot;0&quot;<br /> |- valign=top align=&quot;center&quot; bgcolor=&quot;#eeeeee&quot; <br /> | Наименование || Область применения<br /> |-<br /> | [[Jimi - обработка изображений | JIMI]] || Обработка изображений<br /> |}<br /> <br /> === Технологии, используемые в проектах ===<br /> * [[XML]]<br /> * [[XSLT]]<br /> * [[FO(P)]]<br /> * [[REGEXP]]<br /> * [[MySQL REGEXP]]<br /> <br /> === Вспомогательные технологии ===<br /> * [[Сборщик Apache ANT]]<br /> * [[Сбор и анализ сетевого трафика]]<br /> * [[SSH]]<br /> * [[Оптимизация запросов в MySQL]]<br /> <br /> === Требования BiTel к оформлению ===<br /> * [[Java кода]]<br /> * [[MySQL кода]]</div> Mon, 08 May 2017 04:49:11 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0 Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;/* Ограничения */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * &quot;час&quot; - не &quot;календарный&quot;, а динамический<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> * Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.<br /> <br /> = HOWTO =<br /> &lt;ol&gt;<br /> &lt;li&gt; Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/ &lt;/li&gt;<br /> &lt;li&gt; Делаем ./update.sh в BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt; Создаём таблицы в базе:<br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_inet_quota_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `name` varchar(64) NOT NULL,<br /> `expireTime` bigint(20) NOT NULL,<br /> `penaltyExpiredTime` bigint(20) NOT NULL,<br /> `params` varchar(256) DEFAULT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`)<br /> )<br /> <br /> CREATE TABLE `custom_inet_quota_slices_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `amount` bigint(20) NOT NULL,<br /> `endTime` bigint(20) NOT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`, `endTime`)<br /> )<br /> &lt;/source&gt;<br /> где [MID] - id модуля Inet<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_access.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;access&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_accounting.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;accounting&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt; Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt;Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки &quot;starting QuotaCollector&quot;&lt;/li&gt;<br /> &lt;li&gt;Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:<br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Preferences;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;<br /> <br /> import java.util.Set;<br /> <br /> public class QuotaProfileTariffTreeNode extends TariffTreeNode&lt;InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext&gt; {<br /> <br /> private final QuotaCollector quotaCollector;<br /> //Имя профиля<br /> private final String name;<br /> private final ParameterMap params;<br /> <br /> public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {<br /> super(id, parameterMap);<br /> this.params = new Preferences(parameterMap, &quot;&quot;,&quot;\n&quot;);//parameterMap;<br /> this.name = parameterMap.get(&quot;name&quot;, &quot;&quot;);//null - дефолтный профиль, самый последний по порядку<br /> this.quotaCollector = QuotaCollector.getInstance();<br /> this.logger.debug(&quot;QuotaProfileTariffTreeNode created for nodeId=&quot;+id);<br /> }<br /> <br /> /**<br /> * @param treeNodeId<br /> * @param parentTreeNodeId<br /> * @param req<br /> * @param ctx<br /> * @param treeContext<br /> * @param workerContext<br /> * @return<br /> */<br /> @Override<br /> protected int executeImpl(Long treeNodeId,<br /> Long parentTreeNodeId,<br /> InetTariffRequest req,<br /> InetTariffContext ctx,<br /> TreeContext treeContext,<br /> InetTariffWorkerContext workerContext){<br /> <br /> //this.logger.debug(&quot;execute name = '&quot;+this.name+&quot;' (realtime=&quot;+ctx.realtime+&quot;)&quot;);<br /> //Узел работает только в режиме realtime!<br /> if(!ctx.realtime){<br /> return 0;<br /> }<br /> <br /> if(null==quotaCollector){<br /> this.logger.debug(&quot;quotaCollector == null&quot;);<br /> return 0;<br /> }<br /> <br /> //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -<br /> //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,<br /> //не заходили ли мы уже в какой-либо из вышестоящих<br /> Set&lt;Long&gt; acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);<br /> if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время<br /> //this.logger.debug(&quot;acceptedSet = &quot;+acceptedSet+&quot; contains &quot;+parentTreeNodeId);<br /> return 0;<br /> }<br /> <br /> int servId = req.inetServRuntime.getInetServ().getId();<br /> <br /> QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);<br /> <br /> if (quotaHolder!=null &amp;&amp; this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем<br /> if(quotaHolder.quota==null){<br /> //нужно создать квоту!<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> }else{<br /> logger.debug(&quot;[&quot;+this.name+&quot;] servId=&quot;+servId+&quot; amount consumed = &quot;+quotaHolder.quota.getTotalAmount()+&quot;/&quot;+quotaHolder.quota.quotaSize);<br /> quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;<br /> }<br /> <br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }else if(null == this.name || &quot;&quot;.equals(this.name) || &quot;default&quot;.equals(this.name)){//Дефолтный профиль - обрабатываем<br /> //нужно создать квоту!<br /> this.logger.debug(&quot;default quota profile&quot;);<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }<br /> return 0;<br /> }<br /> }<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Конфигурируем тарифный план:<br /> [[Файл:quotas.png]]<br /> <br /> Вот кнофиги всех профилей квот (узлов тарифа &quot;Обработчик&quot;):<br /> <br /> * 10Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=10Mbit<br /> penalty.period=3600<br /> profile.up=25Mbit<br /> profile.up.349525333=default<br /> profile.up.699050666=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 25Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=25Mbit<br /> penalty.period=3600<br /> profile.down=10Mbit<br /> profile.up=50Mbit<br /> profile.up.699050666=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 50Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=50Mbit<br /> penalty.period=3600<br /> profile.down=25Mbit<br /> profile.up=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 100Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=default<br /> profile.down=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> <br /> &lt;li&gt;Жмём &quot;Оповестить об изменениях&quot;&lt;/li&gt;<br /> &lt;li&gt;Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:<br /> &lt;source lang=bash&gt;<br /> mq.log:07-25/16:58:08 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: default -&gt; 2Mbit (224208201/209715200 bytes consumed)<br /> mq.log:07-25/17:34:31 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 2Mbit -&gt; 1Mbit (212873529/209715200 bytes consumed)<br /> mq.log:07-25/18:27:07 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 1Mbit -&gt; 512Kbit (215569862/209715200 bytes consumed)<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> = Исходники =<br /> https://github.com/Cromeshnic/BG-Quota-Manager<br /> <br /> = В следующих сериях =<br /> * Подробнее расписать параметры конфигурации профилей квот<br /> * Описать механизм работы<br /> * Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 09:34, 28 июля 2015 (UTC)</div> Tue, 28 Jul 2015 09:43:33 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;/* Постановка задачи */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * &quot;час&quot; - не &quot;календарный&quot;, а динамический<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> * Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.<br /> <br /> = HOWTO =<br /> &lt;ol&gt;<br /> &lt;li&gt; Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/ &lt;/li&gt;<br /> &lt;li&gt; Делаем ./update.sh в BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt; Создаём таблицы в базе:<br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_inet_quota_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `name` varchar(64) NOT NULL,<br /> `expireTime` bigint(20) NOT NULL,<br /> `penaltyExpiredTime` bigint(20) NOT NULL,<br /> `params` varchar(256) DEFAULT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`)<br /> )<br /> <br /> CREATE TABLE `custom_inet_quota_slices_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `amount` bigint(20) NOT NULL,<br /> `endTime` bigint(20) NOT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`, `endTime`)<br /> )<br /> &lt;/source&gt;<br /> где [MID] - id модуля Inet<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_access.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;access&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_accounting.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;accounting&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt; Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt;Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки &quot;starting QuotaCollector&quot;&lt;/li&gt;<br /> &lt;li&gt;Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:<br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Preferences;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;<br /> <br /> import java.util.Set;<br /> <br /> public class QuotaProfileTariffTreeNode extends TariffTreeNode&lt;InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext&gt; {<br /> <br /> private final QuotaCollector quotaCollector;<br /> //Имя профиля<br /> private final String name;<br /> private final ParameterMap params;<br /> <br /> public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {<br /> super(id, parameterMap);<br /> this.params = new Preferences(parameterMap, &quot;&quot;,&quot;\n&quot;);//parameterMap;<br /> this.name = parameterMap.get(&quot;name&quot;, &quot;&quot;);//null - дефолтный профиль, самый последний по порядку<br /> this.quotaCollector = QuotaCollector.getInstance();<br /> this.logger.debug(&quot;QuotaProfileTariffTreeNode created for nodeId=&quot;+id);<br /> }<br /> <br /> /**<br /> * @param treeNodeId<br /> * @param parentTreeNodeId<br /> * @param req<br /> * @param ctx<br /> * @param treeContext<br /> * @param workerContext<br /> * @return<br /> */<br /> @Override<br /> protected int executeImpl(Long treeNodeId,<br /> Long parentTreeNodeId,<br /> InetTariffRequest req,<br /> InetTariffContext ctx,<br /> TreeContext treeContext,<br /> InetTariffWorkerContext workerContext){<br /> <br /> //this.logger.debug(&quot;execute name = '&quot;+this.name+&quot;' (realtime=&quot;+ctx.realtime+&quot;)&quot;);<br /> //Узел работает только в режиме realtime!<br /> if(!ctx.realtime){<br /> return 0;<br /> }<br /> <br /> if(null==quotaCollector){<br /> this.logger.debug(&quot;quotaCollector == null&quot;);<br /> return 0;<br /> }<br /> <br /> //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -<br /> //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,<br /> //не заходили ли мы уже в какой-либо из вышестоящих<br /> Set&lt;Long&gt; acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);<br /> if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время<br /> //this.logger.debug(&quot;acceptedSet = &quot;+acceptedSet+&quot; contains &quot;+parentTreeNodeId);<br /> return 0;<br /> }<br /> <br /> int servId = req.inetServRuntime.getInetServ().getId();<br /> <br /> QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);<br /> <br /> if (quotaHolder!=null &amp;&amp; this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем<br /> if(quotaHolder.quota==null){<br /> //нужно создать квоту!<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> }else{<br /> logger.debug(&quot;[&quot;+this.name+&quot;] servId=&quot;+servId+&quot; amount consumed = &quot;+quotaHolder.quota.getTotalAmount()+&quot;/&quot;+quotaHolder.quota.quotaSize);<br /> quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;<br /> }<br /> <br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }else if(null == this.name || &quot;&quot;.equals(this.name) || &quot;default&quot;.equals(this.name)){//Дефолтный профиль - обрабатываем<br /> //нужно создать квоту!<br /> this.logger.debug(&quot;default quota profile&quot;);<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }<br /> return 0;<br /> }<br /> }<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Конфигурируем тарифный план:<br /> [[Файл:quotas.png]]<br /> <br /> Вот кнофиги всех профилей квот (узлов тарифа &quot;Обработчик&quot;):<br /> <br /> * 10Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=10Mbit<br /> penalty.period=3600<br /> profile.up=25Mbit<br /> profile.up.349525333=default<br /> profile.up.699050666=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 25Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=25Mbit<br /> penalty.period=3600<br /> profile.down=10Mbit<br /> profile.up=50Mbit<br /> profile.up.699050666=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 50Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=50Mbit<br /> penalty.period=3600<br /> profile.down=25Mbit<br /> profile.up=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 100Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=default<br /> profile.down=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> <br /> &lt;li&gt;Жмём &quot;Оповестить об изменениях&quot;&lt;/li&gt;<br /> &lt;li&gt;Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:<br /> &lt;source lang=bash&gt;<br /> mq.log:07-25/16:58:08 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: default -&gt; 2Mbit (224208201/209715200 bytes consumed)<br /> mq.log:07-25/17:34:31 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 2Mbit -&gt; 1Mbit (212873529/209715200 bytes consumed)<br /> mq.log:07-25/18:27:07 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 1Mbit -&gt; 512Kbit (215569862/209715200 bytes consumed)<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> = Исходники =<br /> https://github.com/Cromeshnic/BG-Quota-Manager<br /> <br /> = В следующих сериях =<br /> * Подробнее расписать параметры конфигурации профилей квот<br /> * Описать механизм работы<br /> * Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 09:34, 28 июля 2015 (UTC)</div> Tue, 28 Jul 2015 09:43:21 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> * Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.<br /> <br /> = HOWTO =<br /> &lt;ol&gt;<br /> &lt;li&gt; Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/ &lt;/li&gt;<br /> &lt;li&gt; Делаем ./update.sh в BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt; Создаём таблицы в базе:<br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_inet_quota_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `name` varchar(64) NOT NULL,<br /> `expireTime` bigint(20) NOT NULL,<br /> `penaltyExpiredTime` bigint(20) NOT NULL,<br /> `params` varchar(256) DEFAULT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`)<br /> )<br /> <br /> CREATE TABLE `custom_inet_quota_slices_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `amount` bigint(20) NOT NULL,<br /> `endTime` bigint(20) NOT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`, `endTime`)<br /> )<br /> &lt;/source&gt;<br /> где [MID] - id модуля Inet<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_access.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;access&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_accounting.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;accounting&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt; Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt;Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки &quot;starting QuotaCollector&quot;&lt;/li&gt;<br /> &lt;li&gt;Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:<br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Preferences;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;<br /> <br /> import java.util.Set;<br /> <br /> public class QuotaProfileTariffTreeNode extends TariffTreeNode&lt;InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext&gt; {<br /> <br /> private final QuotaCollector quotaCollector;<br /> //Имя профиля<br /> private final String name;<br /> private final ParameterMap params;<br /> <br /> public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {<br /> super(id, parameterMap);<br /> this.params = new Preferences(parameterMap, &quot;&quot;,&quot;\n&quot;);//parameterMap;<br /> this.name = parameterMap.get(&quot;name&quot;, &quot;&quot;);//null - дефолтный профиль, самый последний по порядку<br /> this.quotaCollector = QuotaCollector.getInstance();<br /> this.logger.debug(&quot;QuotaProfileTariffTreeNode created for nodeId=&quot;+id);<br /> }<br /> <br /> /**<br /> * @param treeNodeId<br /> * @param parentTreeNodeId<br /> * @param req<br /> * @param ctx<br /> * @param treeContext<br /> * @param workerContext<br /> * @return<br /> */<br /> @Override<br /> protected int executeImpl(Long treeNodeId,<br /> Long parentTreeNodeId,<br /> InetTariffRequest req,<br /> InetTariffContext ctx,<br /> TreeContext treeContext,<br /> InetTariffWorkerContext workerContext){<br /> <br /> //this.logger.debug(&quot;execute name = '&quot;+this.name+&quot;' (realtime=&quot;+ctx.realtime+&quot;)&quot;);<br /> //Узел работает только в режиме realtime!<br /> if(!ctx.realtime){<br /> return 0;<br /> }<br /> <br /> if(null==quotaCollector){<br /> this.logger.debug(&quot;quotaCollector == null&quot;);<br /> return 0;<br /> }<br /> <br /> //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -<br /> //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,<br /> //не заходили ли мы уже в какой-либо из вышестоящих<br /> Set&lt;Long&gt; acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);<br /> if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время<br /> //this.logger.debug(&quot;acceptedSet = &quot;+acceptedSet+&quot; contains &quot;+parentTreeNodeId);<br /> return 0;<br /> }<br /> <br /> int servId = req.inetServRuntime.getInetServ().getId();<br /> <br /> QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);<br /> <br /> if (quotaHolder!=null &amp;&amp; this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем<br /> if(quotaHolder.quota==null){<br /> //нужно создать квоту!<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> }else{<br /> logger.debug(&quot;[&quot;+this.name+&quot;] servId=&quot;+servId+&quot; amount consumed = &quot;+quotaHolder.quota.getTotalAmount()+&quot;/&quot;+quotaHolder.quota.quotaSize);<br /> quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;<br /> }<br /> <br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }else if(null == this.name || &quot;&quot;.equals(this.name) || &quot;default&quot;.equals(this.name)){//Дефолтный профиль - обрабатываем<br /> //нужно создать квоту!<br /> this.logger.debug(&quot;default quota profile&quot;);<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }<br /> return 0;<br /> }<br /> }<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Конфигурируем тарифный план:<br /> [[Файл:quotas.png]]<br /> <br /> Вот кнофиги всех профилей квот (узлов тарифа &quot;Обработчик&quot;):<br /> <br /> * 10Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=10Mbit<br /> penalty.period=3600<br /> profile.up=25Mbit<br /> profile.up.349525333=default<br /> profile.up.699050666=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 25Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=25Mbit<br /> penalty.period=3600<br /> profile.down=10Mbit<br /> profile.up=50Mbit<br /> profile.up.699050666=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 50Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=50Mbit<br /> penalty.period=3600<br /> profile.down=25Mbit<br /> profile.up=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 100Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=default<br /> profile.down=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> <br /> &lt;li&gt;Жмём &quot;Оповестить об изменениях&quot;&lt;/li&gt;<br /> &lt;li&gt;Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:<br /> &lt;source lang=bash&gt;<br /> mq.log:07-25/16:58:08 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: default -&gt; 2Mbit (224208201/209715200 bytes consumed)<br /> mq.log:07-25/17:34:31 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 2Mbit -&gt; 1Mbit (212873529/209715200 bytes consumed)<br /> mq.log:07-25/18:27:07 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 1Mbit -&gt; 512Kbit (215569862/209715200 bytes consumed)<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> = Исходники =<br /> https://github.com/Cromeshnic/BG-Quota-Manager<br /> <br /> = В следующих сериях =<br /> * Подробнее расписать параметры конфигурации профилей квот<br /> * Описать механизм работы<br /> * Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 09:34, 28 июля 2015 (UTC)</div> Tue, 28 Jul 2015 09:34:23 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;/* HOWTO */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> * Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.<br /> <br /> = HOWTO =<br /> &lt;ol&gt;<br /> &lt;li&gt; Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/ &lt;/li&gt;<br /> &lt;li&gt; Делаем ./update.sh в BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt; Создаём таблицы в базе:<br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_inet_quota_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `name` varchar(64) NOT NULL,<br /> `expireTime` bigint(20) NOT NULL,<br /> `penaltyExpiredTime` bigint(20) NOT NULL,<br /> `params` varchar(256) DEFAULT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`)<br /> )<br /> <br /> CREATE TABLE `custom_inet_quota_slices_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `amount` bigint(20) NOT NULL,<br /> `endTime` bigint(20) NOT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`, `endTime`)<br /> )<br /> &lt;/source&gt;<br /> где [MID] - id модуля Inet<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_access.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;access&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_accounting.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;accounting&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt; Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt;Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки &quot;starting QuotaCollector&quot;&lt;/li&gt;<br /> &lt;li&gt;Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:<br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Preferences;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;<br /> <br /> import java.util.Set;<br /> <br /> public class QuotaProfileTariffTreeNode extends TariffTreeNode&lt;InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext&gt; {<br /> <br /> private final QuotaCollector quotaCollector;<br /> //Имя профиля<br /> private final String name;<br /> private final ParameterMap params;<br /> <br /> public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {<br /> super(id, parameterMap);<br /> this.params = new Preferences(parameterMap, &quot;&quot;,&quot;\n&quot;);//parameterMap;<br /> this.name = parameterMap.get(&quot;name&quot;, &quot;&quot;);//null - дефолтный профиль, самый последний по порядку<br /> this.quotaCollector = QuotaCollector.getInstance();<br /> this.logger.debug(&quot;QuotaProfileTariffTreeNode created for nodeId=&quot;+id);<br /> }<br /> <br /> /**<br /> * @param treeNodeId<br /> * @param parentTreeNodeId<br /> * @param req<br /> * @param ctx<br /> * @param treeContext<br /> * @param workerContext<br /> * @return<br /> */<br /> @Override<br /> protected int executeImpl(Long treeNodeId,<br /> Long parentTreeNodeId,<br /> InetTariffRequest req,<br /> InetTariffContext ctx,<br /> TreeContext treeContext,<br /> InetTariffWorkerContext workerContext){<br /> <br /> //this.logger.debug(&quot;execute name = '&quot;+this.name+&quot;' (realtime=&quot;+ctx.realtime+&quot;)&quot;);<br /> //Узел работает только в режиме realtime!<br /> if(!ctx.realtime){<br /> return 0;<br /> }<br /> <br /> if(null==quotaCollector){<br /> this.logger.debug(&quot;quotaCollector == null&quot;);<br /> return 0;<br /> }<br /> <br /> //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -<br /> //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,<br /> //не заходили ли мы уже в какой-либо из вышестоящих<br /> Set&lt;Long&gt; acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);<br /> if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время<br /> //this.logger.debug(&quot;acceptedSet = &quot;+acceptedSet+&quot; contains &quot;+parentTreeNodeId);<br /> return 0;<br /> }<br /> <br /> int servId = req.inetServRuntime.getInetServ().getId();<br /> <br /> QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);<br /> <br /> if (quotaHolder!=null &amp;&amp; this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем<br /> if(quotaHolder.quota==null){<br /> //нужно создать квоту!<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> }else{<br /> logger.debug(&quot;[&quot;+this.name+&quot;] servId=&quot;+servId+&quot; amount consumed = &quot;+quotaHolder.quota.getTotalAmount()+&quot;/&quot;+quotaHolder.quota.quotaSize);<br /> quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;<br /> }<br /> <br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }else if(null == this.name || &quot;&quot;.equals(this.name) || &quot;default&quot;.equals(this.name)){//Дефолтный профиль - обрабатываем<br /> //нужно создать квоту!<br /> this.logger.debug(&quot;default quota profile&quot;);<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }<br /> return 0;<br /> }<br /> }<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Конфигурируем тарифный план:<br /> [[Файл:quotas.png]]<br /> <br /> Вот кнофиги всех профилей квот (узлов тарифа &quot;Обработчик&quot;):<br /> <br /> * 10Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=10Mbit<br /> penalty.period=3600<br /> profile.up=25Mbit<br /> profile.up.349525333=default<br /> profile.up.699050666=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 25Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=25Mbit<br /> penalty.period=3600<br /> profile.down=10Mbit<br /> profile.up=50Mbit<br /> profile.up.699050666=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 50Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=50Mbit<br /> penalty.period=3600<br /> profile.down=25Mbit<br /> profile.up=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 100Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=default<br /> profile.down=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> <br /> &lt;li&gt;Жмём &quot;Оповестить об изменениях&quot;&lt;/li&gt;<br /> &lt;li&gt;Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:<br /> &lt;source lang=bash&gt;<br /> mq.log:07-25/16:58:08 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: default -&gt; 2Mbit (224208201/209715200 bytes consumed)<br /> mq.log:07-25/17:34:31 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 2Mbit -&gt; 1Mbit (212873529/209715200 bytes consumed)<br /> mq.log:07-25/18:27:07 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 1Mbit -&gt; 512Kbit (215569862/209715200 bytes consumed)<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> = Исходники =<br /> https://github.com/Cromeshnic/BG-Quota-Manager<br /> <br /> = В следующих сериях =<br /> * Подробнее расписать параметры конфигурации профилей квот<br /> * Описать механизм работы<br /> * Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов</div> Tue, 28 Jul 2015 09:33:48 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;/* HOWTO */</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> * Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.<br /> <br /> = HOWTO =<br /> &lt;ol&gt;<br /> &lt;li&gt; Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/ &lt;/li&gt;<br /> &lt;li&gt; Делаем ./update.sh в BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt; Создаём таблицы в базе:<br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_inet_quota_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `name` varchar(64) NOT NULL,<br /> `expireTime` bigint(20) NOT NULL,<br /> `penaltyExpiredTime` bigint(20) NOT NULL,<br /> `params` varchar(256) DEFAULT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`)<br /> )<br /> <br /> CREATE TABLE `custom_inet_quota_slices_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `amount` bigint(20) NOT NULL,<br /> `endTime` bigint(20) NOT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`, `endTime`)<br /> )<br /> &lt;/source&gt;<br /> где [MID] - id модуля Inet<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_access.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;access&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_accounting.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;accounting&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt; Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt;Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки &quot;starting QuotaCollector&quot;&lt;/li&gt;<br /> &lt;li&gt;Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:<br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Preferences;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;<br /> <br /> import java.util.Set;<br /> <br /> public class QuotaProfileTariffTreeNode extends TariffTreeNode&lt;InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext&gt; {<br /> <br /> private final QuotaCollector quotaCollector;<br /> //Имя профиля<br /> private final String name;<br /> private final ParameterMap params;<br /> <br /> public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {<br /> super(id, parameterMap);<br /> this.params = new Preferences(parameterMap, &quot;&quot;,&quot;\n&quot;);//parameterMap;<br /> this.name = parameterMap.get(&quot;name&quot;, &quot;&quot;);//null - дефолтный профиль, самый последний по порядку<br /> this.quotaCollector = QuotaCollector.getInstance();<br /> this.logger.debug(&quot;QuotaProfileTariffTreeNode created for nodeId=&quot;+id);<br /> }<br /> <br /> /**<br /> * @param treeNodeId<br /> * @param parentTreeNodeId<br /> * @param req<br /> * @param ctx<br /> * @param treeContext<br /> * @param workerContext<br /> * @return<br /> */<br /> @Override<br /> protected int executeImpl(Long treeNodeId,<br /> Long parentTreeNodeId,<br /> InetTariffRequest req,<br /> InetTariffContext ctx,<br /> TreeContext treeContext,<br /> InetTariffWorkerContext workerContext){<br /> <br /> //this.logger.debug(&quot;execute name = '&quot;+this.name+&quot;' (realtime=&quot;+ctx.realtime+&quot;)&quot;);<br /> //Узел работает только в режиме realtime!<br /> if(!ctx.realtime){<br /> return 0;<br /> }<br /> <br /> if(null==quotaCollector){<br /> this.logger.debug(&quot;quotaCollector == null&quot;);<br /> return 0;<br /> }<br /> <br /> //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -<br /> //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,<br /> //не заходили ли мы уже в какой-либо из вышестоящих<br /> Set&lt;Long&gt; acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);<br /> if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время<br /> //this.logger.debug(&quot;acceptedSet = &quot;+acceptedSet+&quot; contains &quot;+parentTreeNodeId);<br /> return 0;<br /> }<br /> <br /> int servId = req.inetServRuntime.getInetServ().getId();<br /> <br /> QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);<br /> <br /> if (quotaHolder!=null &amp;&amp; this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем<br /> if(quotaHolder.quota==null){<br /> //нужно создать квоту!<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> }else{<br /> logger.debug(&quot;[&quot;+this.name+&quot;] servId=&quot;+servId+&quot; amount consumed = &quot;+quotaHolder.quota.getTotalAmount()+&quot;/&quot;+quotaHolder.quota.quotaSize);<br /> quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;<br /> }<br /> <br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }else if(null == this.name || &quot;&quot;.equals(this.name) || &quot;default&quot;.equals(this.name)){//Дефолтный профиль - обрабатываем<br /> //нужно создать квоту!<br /> this.logger.debug(&quot;default quota profile&quot;);<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }<br /> return 0;<br /> }<br /> }<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Конфигурируем тарифный план:<br /> [[Файл:quotas.png]]<br /> <br /> Вот кнофиги всех профилей квот (узлов тарифа &quot;Обработчик&quot;):<br /> <br /> * 10Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=10Mbit<br /> penalty.period=3600<br /> profile.up=25Mbit<br /> profile.up.349525333=default<br /> profile.up.699050666=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 25Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=25Mbit<br /> penalty.period=3600<br /> profile.down=10Mbit<br /> profile.up=50Mbit<br /> profile.up.699050666=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 50Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=50Mbit<br /> penalty.period=3600<br /> profile.down=25Mbit<br /> profile.up=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 100Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=default<br /> profile.down=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> <br /> &lt;li&gt;Жмём &quot;Оповестить об изменениях&quot;&lt;/li&gt;<br /> &lt;li&gt;Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:<br /> &lt;source lang=txt&gt;<br /> mq.log:07-25/16:58:08 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: default -&gt; 2Mbit (224208201/209715200 bytes consumed)<br /> mq.log:07-25/17:34:31 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 2Mbit -&gt; 1Mbit (212873529/209715200 bytes consumed)<br /> mq.log:07-25/18:27:07 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 1Mbit -&gt; 512Kbit (215569862/209715200 bytes consumed)<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> = Исходники =<br /> https://github.com/Cromeshnic/BG-Quota-Manager<br /> <br /> = В следующих сериях =<br /> * Подробнее расписать параметры конфигурации профилей квот<br /> * Описать механизм работы<br /> * Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов</div> Tue, 28 Jul 2015 09:33:23 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> * Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.<br /> <br /> = HOWTO =<br /> &lt;ol&gt;<br /> &lt;li&gt; Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/ &lt;/li&gt;<br /> &lt;li&gt; Делаем ./update.sh в BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt; Создаём таблицы в базе:<br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_inet_quota_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `name` varchar(64) NOT NULL,<br /> `expireTime` bigint(20) NOT NULL,<br /> `penaltyExpiredTime` bigint(20) NOT NULL,<br /> `params` varchar(256) DEFAULT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`)<br /> )<br /> <br /> CREATE TABLE `custom_inet_quota_slices_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `amount` bigint(20) NOT NULL,<br /> `endTime` bigint(20) NOT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`, `endTime`)<br /> )<br /> &lt;/source&gt;<br /> где [MID] - id модуля Inet<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_access.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;access&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_accounting.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;accounting&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt; Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt;Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки &quot;starting QuotaCollector&quot;&lt;/li&gt;<br /> &lt;li&gt;Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:<br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Preferences;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;<br /> <br /> import java.util.Set;<br /> <br /> public class QuotaProfileTariffTreeNode extends TariffTreeNode&lt;InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext&gt; {<br /> <br /> private final QuotaCollector quotaCollector;<br /> //Имя профиля<br /> private final String name;<br /> private final ParameterMap params;<br /> <br /> public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {<br /> super(id, parameterMap);<br /> this.params = new Preferences(parameterMap, &quot;&quot;,&quot;\n&quot;);//parameterMap;<br /> this.name = parameterMap.get(&quot;name&quot;, &quot;&quot;);//null - дефолтный профиль, самый последний по порядку<br /> this.quotaCollector = QuotaCollector.getInstance();<br /> this.logger.debug(&quot;QuotaProfileTariffTreeNode created for nodeId=&quot;+id);<br /> }<br /> <br /> /**<br /> * @param treeNodeId<br /> * @param parentTreeNodeId<br /> * @param req<br /> * @param ctx<br /> * @param treeContext<br /> * @param workerContext<br /> * @return<br /> */<br /> @Override<br /> protected int executeImpl(Long treeNodeId,<br /> Long parentTreeNodeId,<br /> InetTariffRequest req,<br /> InetTariffContext ctx,<br /> TreeContext treeContext,<br /> InetTariffWorkerContext workerContext){<br /> <br /> //this.logger.debug(&quot;execute name = '&quot;+this.name+&quot;' (realtime=&quot;+ctx.realtime+&quot;)&quot;);<br /> //Узел работает только в режиме realtime!<br /> if(!ctx.realtime){<br /> return 0;<br /> }<br /> <br /> if(null==quotaCollector){<br /> this.logger.debug(&quot;quotaCollector == null&quot;);<br /> return 0;<br /> }<br /> <br /> //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -<br /> //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,<br /> //не заходили ли мы уже в какой-либо из вышестоящих<br /> Set&lt;Long&gt; acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);<br /> if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время<br /> //this.logger.debug(&quot;acceptedSet = &quot;+acceptedSet+&quot; contains &quot;+parentTreeNodeId);<br /> return 0;<br /> }<br /> <br /> int servId = req.inetServRuntime.getInetServ().getId();<br /> <br /> QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);<br /> <br /> if (quotaHolder!=null &amp;&amp; this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем<br /> if(quotaHolder.quota==null){<br /> //нужно создать квоту!<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> }else{<br /> logger.debug(&quot;[&quot;+this.name+&quot;] servId=&quot;+servId+&quot; amount consumed = &quot;+quotaHolder.quota.getTotalAmount()+&quot;/&quot;+quotaHolder.quota.quotaSize);<br /> quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;<br /> }<br /> <br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }else if(null == this.name || &quot;&quot;.equals(this.name) || &quot;default&quot;.equals(this.name)){//Дефолтный профиль - обрабатываем<br /> //нужно создать квоту!<br /> this.logger.debug(&quot;default quota profile&quot;);<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }<br /> return 0;<br /> }<br /> }<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Конфигурируем тарифный план:<br /> [[Файл:quotas.png]]<br /> <br /> Вот кнофиги всех профилей квот (узлов тарифа &quot;Обработчик&quot;):<br /> <br /> * 10Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=10Mbit<br /> penalty.period=3600<br /> profile.up=25Mbit<br /> profile.up.349525333=default<br /> profile.up.699050666=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 25Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=25Mbit<br /> penalty.period=3600<br /> profile.down=10Mbit<br /> profile.up=50Mbit<br /> profile.up.699050666=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 50Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=50Mbit<br /> penalty.period=3600<br /> profile.down=25Mbit<br /> profile.up=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 100Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=default<br /> profile.down=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> <br /> &lt;li&gt;Жмём &quot;Оповестить об изменениях&quot;&lt;/li&gt;<br /> &lt;li&gt;Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:<br /> mq.log:07-25/16:58:08 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: default -&gt; 2Mbit (224208201/209715200 bytes consumed)<br /> mq.log:07-25/17:34:31 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 2Mbit -&gt; 1Mbit (212873529/209715200 bytes consumed)<br /> mq.log:07-25/18:27:07 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 1Mbit -&gt; 512Kbit (215569862/209715200 bytes consumed)<br /> &lt;/li&gt;<br /> &lt;li&gt;Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> = Исходники =<br /> https://github.com/Cromeshnic/BG-Quota-Manager<br /> <br /> = В следующих сериях =<br /> * Подробнее расписать параметры конфигурации профилей квот<br /> * Описать механизм работы<br /> * Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов</div> Tue, 28 Jul 2015 09:32:17 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> * Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.<br /> <br /> = HOWTO =<br /> &lt;ol&gt;<br /> &lt;li&gt; Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/ &lt;/li&gt;<br /> &lt;li&gt; Делаем ./update.sh в BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt; Создаём таблицы в базе:<br /> &lt;source lang=sql&gt;<br /> CREATE TABLE `custom_inet_quota_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `name` varchar(64) NOT NULL,<br /> `expireTime` bigint(20) NOT NULL,<br /> `penaltyExpiredTime` bigint(20) NOT NULL,<br /> `params` varchar(256) DEFAULT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`)<br /> )<br /> <br /> CREATE TABLE `custom_inet_quota_slices_[MID]` (<br /> `servId` int(11) NOT NULL,<br /> `nodeId` bigint(20) NOT NULL,<br /> `amount` bigint(20) NOT NULL,<br /> `endTime` bigint(20) NOT NULL,<br /> PRIMARY KEY (`servId`,`nodeId`, `endTime`)<br /> )<br /> &lt;/source&gt;<br /> где [MID] - id модуля Inet<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_access.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;access&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Прописываем в inet_accounting.xml:<br /> <br /> &lt;source lang=xml&gt;<br /> ...<br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;bean name=&quot;quotaCollector&quot; class=&quot;ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector&quot;&gt;<br /> &lt;param name=&quot;app&quot;&gt;accounting&lt;/param&gt;<br /> &lt;/bean&gt;<br /> ...<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt; Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting &lt;/li&gt;<br /> &lt;li&gt;Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки &quot;starting QuotaCollector&quot;&lt;/li&gt;<br /> &lt;li&gt;Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:<br /> &lt;source lang=java&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;<br /> <br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;<br /> import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;<br /> import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Preferences;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;<br /> import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;<br /> <br /> import java.util.Set;<br /> <br /> public class QuotaProfileTariffTreeNode extends TariffTreeNode&lt;InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext&gt; {<br /> <br /> private final QuotaCollector quotaCollector;<br /> //Имя профиля<br /> private final String name;<br /> private final ParameterMap params;<br /> <br /> public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {<br /> super(id, parameterMap);<br /> this.params = new Preferences(parameterMap, &quot;&quot;,&quot;\n&quot;);//parameterMap;<br /> this.name = parameterMap.get(&quot;name&quot;, &quot;&quot;);//null - дефолтный профиль, самый последний по порядку<br /> this.quotaCollector = QuotaCollector.getInstance();<br /> this.logger.debug(&quot;QuotaProfileTariffTreeNode created for nodeId=&quot;+id);<br /> }<br /> <br /> /**<br /> * @param treeNodeId<br /> * @param parentTreeNodeId<br /> * @param req<br /> * @param ctx<br /> * @param treeContext<br /> * @param workerContext<br /> * @return<br /> */<br /> @Override<br /> protected int executeImpl(Long treeNodeId,<br /> Long parentTreeNodeId,<br /> InetTariffRequest req,<br /> InetTariffContext ctx,<br /> TreeContext treeContext,<br /> InetTariffWorkerContext workerContext){<br /> <br /> //this.logger.debug(&quot;execute name = '&quot;+this.name+&quot;' (realtime=&quot;+ctx.realtime+&quot;)&quot;);<br /> //Узел работает только в режиме realtime!<br /> if(!ctx.realtime){<br /> return 0;<br /> }<br /> <br /> if(null==quotaCollector){<br /> this.logger.debug(&quot;quotaCollector == null&quot;);<br /> return 0;<br /> }<br /> <br /> //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -<br /> //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,<br /> //не заходили ли мы уже в какой-либо из вышестоящих<br /> Set&lt;Long&gt; acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);<br /> if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время<br /> //this.logger.debug(&quot;acceptedSet = &quot;+acceptedSet+&quot; contains &quot;+parentTreeNodeId);<br /> return 0;<br /> }<br /> <br /> int servId = req.inetServRuntime.getInetServ().getId();<br /> <br /> QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);<br /> <br /> if (quotaHolder!=null &amp;&amp; this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем<br /> if(quotaHolder.quota==null){<br /> //нужно создать квоту!<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> }else{<br /> logger.debug(&quot;[&quot;+this.name+&quot;] servId=&quot;+servId+&quot; amount consumed = &quot;+quotaHolder.quota.getTotalAmount()+&quot;/&quot;+quotaHolder.quota.quotaSize);<br /> quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;<br /> }<br /> <br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }else if(null == this.name || &quot;&quot;.equals(this.name) || &quot;default&quot;.equals(this.name)){//Дефолтный профиль - обрабатываем<br /> //нужно создать квоту!<br /> this.logger.debug(&quot;default quota profile&quot;);<br /> try {<br /> quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));<br /> }catch (Exception ex){<br /> logger.error(ex.getMessage(), ex);<br /> }<br /> acceptedSet.add(parentTreeNodeId);<br /> return 1;<br /> }<br /> return 0;<br /> }<br /> }<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> &lt;li&gt;Конфигурируем тарифный план:<br /> [[Файл:quotas.png]]<br /> Вот кнофиги всех профилей квот (узлов тарифа &quot;Обработчик&quot;):<br /> <br /> * 10Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=10Mbit<br /> penalty.period=3600<br /> profile.up=25Mbit<br /> profile.up.349525333=default<br /> profile.up.699050666=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 25Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=25Mbit<br /> penalty.period=3600<br /> profile.down=10Mbit<br /> profile.up=50Mbit<br /> profile.up.699050666=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 50Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=50Mbit<br /> penalty.period=3600<br /> profile.down=25Mbit<br /> profile.up=default<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> <br /> * 100Mbit:<br /> &lt;source lang=ini&gt;<br /> expire.period=3660<br /> name=default<br /> profile.down=50Mbit<br /> quota.size=1048576000<br /> slice.count=6<br /> slice.period=600<br /> traffic.types=1,2<br /> &lt;/source&gt;<br /> &lt;/li&gt;<br /> <br /> &lt;li&gt;Жмём &quot;Оповестить об изменениях&quot;&lt;/li&gt;<br /> &lt;li&gt;Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:<br /> mq.log:07-25/16:58:08 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: default -&gt; 2Mbit (224208201/209715200 bytes consumed)<br /> mq.log:07-25/17:34:31 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 2Mbit -&gt; 1Mbit (212873529/209715200 bytes consumed)<br /> mq.log:07-25/18:27:07 INFO [event-proc-p-2-t-1] QuotaCollector - quota changed for servId=7177: 1Mbit -&gt; 512Kbit (215569862/209715200 bytes consumed)<br /> &lt;/li&gt;<br /> &lt;li&gt;Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот&lt;/li&gt;<br /> &lt;/ol&gt;<br /> <br /> = Исходники =<br /> https://github.com/Cromeshnic/BG-Quota-Manager<br /> <br /> = В следующих сериях =<br /> * Подробнее расписать параметры конфигурации профилей квот<br /> * Описать механизм работы<br /> * Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов</div> Tue, 28 Jul 2015 09:31:00 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Файл:Quotas.png http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Quotas.png <p>Cromeshnic:&#32;</p> <hr /> <div></div> Tue, 28 Jul 2015 09:19:23 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%84%D0%B0%D0%B9%D0%BB%D0%B0:Quotas.png Quota Manager http://wiki.bitel.ru/index.php/Quota_Manager <p>Cromeshnic:&#32;Новая страница: «{{Актуальность Версии|версия=5.2}} = Описание = У Cisco есть приложение - Quota Manager, работающее в св…»</p> <hr /> <div>{{Актуальность Версии|версия=5.2}}<br /> <br /> = Описание =<br /> У Cisco есть приложение - Quota Manager, работающее в связке с SCE.<br /> Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика.<br /> Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана &quot;Диапазон трафика&quot;, но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов.<br /> Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.<br /> <br /> = Постановка задачи =<br /> Нужно сделать тариф на VPN:<br /> * Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит<br /> * Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит<br /> * Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит<br /> * Если клиент на любой &quot;ступеньке&quot; (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх<br /> * Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх<br /> * и т.д.<br /> * &quot;Скачал&quot; - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме<br /> * Время &quot;1 час&quot; на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а &quot;поднимать&quot; уже через полчаса<br /> * Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает &quot;с чистого листа&quot;. Это ещё одна фича, которую нельзя реализовать узлом &quot;диапазон трафика&quot; в Inet.<br /> * На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.<br /> <br /> = Реализация =<br /> Расширение представляет из себя jar-файл ([[Файл:Custom inet quota.zip]]), а также класс-узел тарифного плана в динамическом коде.<br /> Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.<br /> <br /> = Ограничения =<br /> * Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.<br /> * Не тестировалось на версиях BG 6+<br /> <br /> = HOWTO =</div> Tue, 28 Jul 2015 08:38:55 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Quota_Manager Файл:Custom inet quota.zip http://wiki.bitel.ru/index.php/%D0%A4%D0%B0%D0%B9%D0%BB:Custom_inet_quota.zip <p>Cromeshnic:&#32;</p> <hr /> <div></div> Tue, 28 Jul 2015 08:35:14 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%84%D0%B0%D0%B9%D0%BB%D0%B0:Custom_inet_quota.zip Расширения http://wiki.bitel.ru/index.php/%D0%A0%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%B8%D1%8F <p>Cromeshnic:&#32;</p> <hr /> <div>*[[DHCP82 авторизация по MAC-адресу]]<br /> *[[Отображение пакетов трафика на странице Тарифные опции личного кабинета]]<br /> *[[Заказ IP-детализации из личного кабинета]]<br /> *[[Выполнение команд с помощью Обработчика управления устройством]]<br /> *[[Пропуск обработки определенных RADIUS-Accounting запросов]]<br /> *[[Quota Manager]]</div> Tue, 28 Jul 2015 07:58:44 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A0%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%B8%D1%8F Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Описание задачи */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают радиус-сервер для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее вынести эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET1000<br /> cisco-SSG-Service-Info=QU;1000000;D;1000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, мы объединяем в группу MISC и прописываем индивидуально через radius.inetOption.x.attributes<br /> <br /> === Общий алгоритм добавления новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 06:28, 21 июля 2014 (UTC)</div> Tue, 22 Jul 2014 10:01:36 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Vlan per user + Cisco IP subscriber interface + ISG http://wiki.bitel.ru/index.php/Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG <p>Cromeshnic:&#32;/* Дополнительно */</p> <hr /> <div>= Описание задачи =<br /> Клиентам предоставляется доступ по схеме vlan per user на роутерах cisco.<br /> Необходимо реализовать учёт и управление услугами в модуле Inet.<br /> <br /> = Решение =<br /> Сразу оговоримся, что в нашем случае речь идёт об услуге '''MPLS IP VPN'''.<br /> Для доступа интернет, возможно, появятся какие-то нюансы, но в целом механизм тот же.<br /> <br /> Схема решения такая:<br /> *на клиентском интерфейсе настраивается ip subscriber interface + isg<br /> *интерфейсы авторизуются в BGInetAccess по паре &quot;устройство+интерфейс&quot;<br /> *Настройки скорости выдаются через ISG-сервисы, смена тарифа происходит через CoA<br /> *Трафик собираем по радиус-счётчикам ISG-сервисов и родительской сессии, либо через Netflow<br /> *В сервисе модуля Inet указываем только устройство и интерфейс<br /> <br /> В нашем случае в биллинге не задаются ни сеть IP клиента на интерфейсе, ни его VLAN, т.к. для тарификации они не играют роли.<br /> Но это можно реализовать для учёта ресурсов, а также для автоматического конфигурирования.<br /> <br /> ''Дополнительно усложним задачу:<br /> В некоторых случаях клиенту предоставляется VPN + доступ в интернет через NAT на нашей циске.<br /> Доступ в интернет считается в другом модуле, но такая схема несколько усложняет задачу, поскольку тогда в каждой точке VPN-а нужно разделять трафик на собственно VPN и интернет, чтобы не тарифицировать последний 2 раза. Эту схему мы реализуем отдельным типом сервиса с отдельной привязкой трафика и отдельными сервисами ISG.''<br /> <br /> = Настройка биллинга =<br /> == Настройка модуля ==<br /> Для услуг VPN было решено завести отдельный экземпляр модуля Inet с названием 'VPN'.<br /> <br /> Плюсы:<br /> *Можно учитывать услуги интернета и vpn на одном договоре без проблем с пересечением тарифных планов (основная причина, т.к. в наследство осталось много таких договоров)<br /> *Учёт услуг, визуальное разделение услуг на договоре и в отчётах.<br /> <br /> Минусы:<br /> *Дублирование ресурсов между несколькими экземплярами модулей Inet: одни и те же устройства, vlan, интерфейсы используются в разных местах. Могут быть проблемы с учётом.<br /> <br /> Конфигурация модуля:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Активные и приостановленные статусы договора<br /> contract.status.active.codes=0<br /> contract.status.suspend.codes=3,4<br /> # Проверка цены в тарифе: 0 - проверка отсутсвует, 1 - ошибка, только если у сессии есть трафик определенного типа,<br /> # но для него нет цены, 2 - ошибка, если хотя бы для одного типа трафика в привязке типа сервиса нет цены (по умолчанию - 1)<br /> #http://forum.bgbilling.ru/viewtopic.php?p=65629#p65629<br /> accounting.tariffication.checkPrice=0<br /> # Режим активации учетного периода, если не используется скрипт на событие активации,<br /> # 0 (по умолчанию) - активация со дня подключения (старта сессии), 1 - активация с начала месяца.<br /> # Следует учитывать, что учетный период является второй величиной при вычислении пропорциональности<br /> # в тарифной ветке &quot;Диапазон трафика&quot;<br /> #accounting.period.activation.mode=0<br /> <br /> # Нужно ли отключать сервис с типом инициации &quot;по трафику&quot;, если тариф не найден<br /> #serv.disableOnTariffError=0<br /> <br /> #Пункты Web - меню<br /> #web.menuItem1=Отчет по сессиям Inet<br /> #web.menuItem2=Смена пароля на логины Inet<br /> #web.menuItem3=none<br /> #web.menuItem3=Отчет по трафикам Inet<br /> <br /> # Параметры автоматической генерации логина для сервиса.<br /> # Минимальное значение логина при генерации логина<br /> #serv.login.min=1<br /> # Максимальное значение логина при генерации логина (т.е. если в базе присутствуют логины 1,2,3 и 10000000,<br /> # то при генерации создастся логин 4, а не 10000001)<br /> #serv.login.max=9999999<br /> <br /> # Парамерты автоматической генерации пароля для сервиса. Можно указать в конфигурации модуля, конфигурации устройства, конфигурации типа сервиса<br /> # (в последнем случае значения будут главнее):<br /> # Минимальная длина пароля<br /> serv.password.length.min=5<br /> # Максимальная длина пароля<br /> serv.password.length.max=16<br /> # Разрешенные символы (используются также при генерации пароля)<br /> serv.password.chars=1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz<br /> # Описание разрешенных символов, если пользователь ввел другие<br /> serv.password.chars.description=В пароле допустимы только цифры и латинские буквы.<br /> # Длина для автоматически генерируемого пароля<br /> serv.password.length.auto=6<br /> # Используемые символы для автоматически генерируемого пароля (по умолчанию значение берется из параметра serv.password.chars)<br /> #serv.password.chars.auto=<br /> <br /> # Параметры активации карточек модуля card при использовании InetRadiusProcessor,<br /> # данные параметры можно указать как в конфигурации модуля, так и в конфигурации устройства.<br /> # Код модуля card<br /> #card.moduleId=<br /> # id услуг активации<br /> #card.activate.serviceIds=<br /> # Минимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.min=0<br /> # Максимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.max=0<br /> &lt;/source&gt;<br /> <br /> Также не забываем настроить задачу планировщика &quot;Активация/деактивация сервисов по периоду&quot; хотя бы раз в сутки в полночь, чтобы корректно обрабатывалось переоформление и перенос сервисов с договора на договор будущим числом.<br /> <br /> == Типы трафиков и привязки ==<br /> === Типы трафиков ===<br /> <br /> [[Файл:traffic_types.png]]<br /> <br /> === Привязки ===<br /> ==== Radius - full ====<br /> Простая привязка с 2 типами трафика: входящий/исходящий, которые берутся из стандартных счетчиков радиус-пакетов родительской сессии.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> &lt;div class=&quot;collapsible collapsed&quot;&gt;[[Файл:radius-full.png]]&lt;/div&gt;<br /> <br /> ==== Radius - NAT ====<br /> Привязка для VPN + NAT.<br /> Для родительской сессии будет 2 дочерних сессии ISG: IPVPN-NAT-INET и IPVPN-NAT-VPN-xxx, где xxx - скорость.<br /> Будем брать трафики из них.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> [[Файл:radius-nat.png]]<br /> <br /> == Типы сервисов ==<br /> <br /> === VPN-IPoE ===<br /> [[Файл:serv-type-ipoe.png]]<br /> <br /> === VPN-IPoE (+NAT) ===<br /> [[Файл:serv-type-ipoe-nat.png]]<br /> <br /> == Опции ==<br /> Заведём опции для соответствующих ISG-сервисов.<br /> Опции FLOWON/FLOWOFF нужны для включения/выключения netflow на интерфейсе<br /> [[Файл:options.png]]<br /> <br /> == Устройства и ресурсы ==<br /> <br /> === Группы устройств ===<br /> Не используются.<br /> === Типы устройств ===<br /> Мы используем 3 типа устройств:<br /> *Группа (ProcessGroup) - пустой тип устройств. Указывается в качестве рута для BGInetAccess и BGInetAccounting (см соответствующий раздел)<br /> *Город - пустой тип устройства, добавлен для разбиения дерева по городам. В будущем, возможно, к нему будут привязываться специфические ProcessHandler-ы, отдельные конфиги или выделяться свои Access и Accounting сервера для каждого города.<br /> *IPoE - тип устройства для цисок с поддержкой ip subscriber interface + ISG<br /> <br /> [[Файл:devicetype-ipoe.png]]<br /> <br /> Конфиг типа устройства IPoE:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Realm default атрибуты<br /> radius.realm.default.attributes=cisco-SSG-Account-Info=ADEFAULT;cisco-avpair=subscriber:accounting-list=BG-DSI-IPVRF<br /> #коды ошибок, которые обрабатываются системой Reject-To-Accept (то же самое, что и realm.reject.error)<br /> #http://bgbilling.ru/v5.2/doc/ch18s20.html<br /> #-------reject-to-accept отсутствует<br /> #radius.disable.accessCodes=4,10,11,12,44<br /> # Какие адреса выдавать при ответе Access-Accept в состоянии disable: <br /> # 0 (по умолчанию) - из radius.disable.ipCategories, 1 - так же, как если бы не было ошибки (в том числе привязанные к сервису в договоре)<br /> #radius.disable.mode=0<br /> # код категории ресурсов Fake пула<br /> #radius.disable.ipCategories=7<br /> # радиус атрибуты, отправляемые в режиме Reject-To-Accept<br /> #radius.disable.attributes=<br /> # Id фиктивного сервиса, к которому будут привязываться сессии, по которым нормальный сервис не был найден (код ошибки: 1, логин не найден).<br /> # Необходим, если в radius.disable.accessCodes присутствует код 1<br /> #radius.disable.servId=<br /> # Атрибуты, при наличии которых соединение должно считаться в состоянии DISABLE (т.е. с ограниченным доступом)<br /> #radius.disable.pattern.attributes=<br /> # Вендор атрибута, где хранится MAC-адрес<br /> # Берём стандартный NAS-Port-Id<br /> radius.macAddress.vendor=-1<br /> # Код атрибута, где хранится MAC-адрес<br /> # Берём NAS-Port-Id<br /> radius.macAddress.type=87<br /> # Префикс атрибута (если есть), где хранится MAC-адрес. Например, для cisco avpair <br /> #radius.macAddress.prefix=<br /> #Порт для отправки PoD и CoA запросов (по умолчанию - порт, заданный в параметрах устройства Хост/порт)<br /> radius.port=1700<br /> #<br /> # Режим поиска сервиса: 0 (по умолчанию) - по логину, 1 - по интерфейсу на устройстве (в предобработке должны быть<br /> # проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или INTERFACE_ID), 2 - по VLAN на устройстве (в предобработке<br /> # должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID), 4 - по VLAN на устройстве или<br /> # дочернем устройстве (в предобработке должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID),<br /> # 5 - по MAC-адресу на устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS), 6 - по MAC-адресу на<br /> # устройстве или дочернем устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS).<br /> radius.servSearchMode=1,0<br /> #<br /> # Нужно ли проверять пароль: 0 - нет, 1 (по умолчанию) - да.<br /> radius.password.verification=0<br /> #<br /> # При выдаче access-accept добавлять запись в базу<br /> # необходимо, если используется reject-to-accept и по старт пакету нельзя определить в каком состоянии соединение<br /> #чтобы Access при Access-Accept добавлял соединение в базе со статусом WAIT и указанием выданного состояния и опций<br /> connection.start.fromAccept=1<br /> # Бывают ситуации, когда start-пакет не дошел до Accounting-сервера. В этом случае, при<br /> # 1 (значение по умолчанию) - сессия создастся от текущего момента,<br /> # 2 - Accounting проверит, что время сессии из update/stop пакета не больше, чем значение connection.close.timeout и создаст сессию от ее начала, иначе,<br /> # если время сессии больше чем connection.close.timeout, сессия создастся от текущего момента,<br /> # 0 - сессия без старт-пакета создана не будет.<br /> connection.start.fromUpdate=1<br /> # таймаут перевода соединения в статус suspended при остутствии радиус пакетов<br /> connection.suspend.timeout=1200<br /> # таймаут закрытия соединения при остутствии радиус пакетов (не складывается с connection.suspend.timeout)<br /> connection.close.timeout=1260<br /> #При завершении соединения по сигналу Stop-пакетом (RADIUS-Stop) оно фактически завершается через количество секунд, определяемое переменной connection.finish.timeout. Это позволяет, в частности, реализовать сбор &quot;запоздалой&quot; информации о трафике, которая может прийти после Stop-пакета.<br /> connection.finish.timeout=2<br /> # Проверка на повторную аутентификацию при Access-Request. Бывает нужна в случаях, когда NAS сбрасывает (теряет) сессию, но<br /> # Stop-пакет не присылает и клиент пытается подключиться повторно, но у него стоит ограничение на максимум одну сессию. При совпадении<br /> # callingStationId с одной из активных сессий и установленным параметром: 1 - осуществляется попытка закрытия старой сессии (connectionClose),<br /> # 2 - попытка закрытия сессии (connectionClose) и завершение ее в базе, не дожидаясь стоп пакета, 3 - завершение в базе.<br /> #radius.connection.checkDuplicate=0<br /> #<br /> # Нужно ли убирать домен перед поиском сервиса по логину из поля User-Name. По умолчанию - да (1).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> #для IPoE какой пришёл, такой и берём, там не должно быть лишнего<br /> radius.username.removeDomain=0<br /> #<br /> # Нужно ли убирать пробелы из поля User-Name перед поиском логина. По умолчанию - нет (0).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> radius.username.removeWhitespace=0<br /> #<br /> # Шаблон вывода ошибки в мониторе с использованием атрибутов из RADIUS-пакета<br /> #radius.accessError.infoPattern=LOGIN:$User-Name<br /> #<br /> # Параметры активации сервисов<br /> # длина паузы, если возникла ошибка<br /> #sa.error.pause=60<br /> # количество заданий за раз<br /> #sa.batch.size=20<br /> # время (сек) ожидания завершения всех заданий (при асинхронной работе)<br /> #sa.batch.wait=5<br /> # пауза (сек) после обработки заданий<br /> #sa.batch.pause=0<br /> # время (сек) ожидания новой задачи перед вызовом disconnect.<br /> #sa.batch.waitNext=5<br /> #<br /> #----------------------------------------<br /> #параметры обработчика активации сервисов<br /> #----------------------------------------<br /> # откуда при отправке CoA брать атрибуты опций (по умолчанию - те же атрибуты, что выдаются при удачной авторизации)<br /> #sa.radius.option.attributesPrefix=radius.inetOption.<br /> #sa.radius.connection.attributes=NAS-Port, Acct-Session-Id, User-Name, Framed-IP-Address, NAS-IP-Address, NAS-Identifier<br /> sa.radius.connection.attributes=Acct-Session-Id, User-Name<br /> #режим отправки CoA. 0 - команды 0xc и 0xb в одном пакете для всех сервисов, 1 - команды 0xc и 0xb в отдельном пакете для каждого сервиса, 2 - атрибуты subscriber:command= в раздельных пакетах для каждого сервиса<br /> sa.radius.connection.coa.mode=1<br /> #Что делать для закрытия соединения:<br /> # 0 (default) - ничего<br /> # 2 - шлём PoD<br /> # 3 - шлём subscriber:command=account-logoff<br /> sa.radius.connection.close.mode=3<br /> #если dhcp lease time большой, а при положительном балансе доступ нужно дать (даже если адрес сейчас выдан серый), нужно установить 1<br /> sa.radius.connection.coa.onEnable=0<br /> #атрибуты CoA запроса для прекращения доступа (используется при sa.radius.connection.withoutBreak=1)<br /> #sa.radius.disable.attributes={@radius.disable.attributes}<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой CoA<br /> #sa.radius.coa.attributes=<br /> #добавлять ли при отправке CoA атрибуты реалма (для default - из radius.realm.default.attributes)<br /> #sa.radius.realm.addAttributes=0<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой PoD<br /> #sa.radius.pod.attributes=<br /> #<br /> #<br /> ###### VPN services ######<br /> radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle<br /> #Сопоставление nas-port-id из запроса с id порта в биллинге по имени интерфейса (см ru.dsi.bgbilling.modules.inet.dyn.device.cisco.ISGIPoEProtocolHandler)<br /> radius.ipoe.nas_port_id.pattern.1.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{1,4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.1.replacement=$1/0/$2/$3<br /> radius.ipoe.nas_port_id.pattern.2.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{8}$)0{0,3}([1-9]\d{0,3})(?=\d{4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.2.replacement=$1/0/$2/$4.$3<br /> radius.ipoe.nas_port_id.pattern.3.pattern=^BD(\d+)$<br /> radius.ipoe.nas_port_id.pattern.3.replacement=255/0/$1<br /> &lt;/source&gt;<br /> <br /> Обратите внимание на строчку:<br /> &lt;source lang=&quot;ini&quot;&gt;radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle&lt;/source&gt;<br /> Здесь мы говорим, что все опции модуля, находящиеся в ветке с id=1 (&quot;Группа: VPN&quot;), следует трактовать как сервисы ISG с соответствующим названием.<br /> <br /> Параметры radius.ipoe.nas_port_id.pattern.* нужны для самописного предобработчика радиус-пакетов ISGIPoEProtocolHandler.<br /> В них определяются regexp-шаблоны, по которым сопоставляется значение радиус-атрибута Nas-Port-Id (например, 0/0/0/1112) и название интерфейса в биллинге (Gi0/0.1112).<br /> Подробнее см. описание класса.<br /> <br /> ==== ServciceActivator ====<br /> Для нашей схемы используется модифицированный ISGServciceActivator.<br /> <br /> Отличия от стандартного Бителовского:<br /> - соответствие &quot;опция - сервис ISG&quot; берётся из optionRadiusAttributesMap, чтобы работали новые шаблоны атрибутов (radius.inetOption.1.template)<br /> - в соответствие &quot;опция - сервис ISG&quot; добавлена зависимость от realm-а<br /> - убрано всё, что касается DHCP<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorEvent;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetConnection;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntimeMap;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> <br /> import java.util.*;<br /> <br /> /**<br /> * Конфигурация устройства:<br /> * sa.radius.connection.coa.mode = 1<br /> * режим отправки CoA:<br /> * 0 - отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> * 1 - (default) отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> * 2 - отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> *<br /> * sa.radius.service.disable =<br /> * имена сервисов, при котором доступ отключен<br /> * отправляются в режиме Reject-To-Accept<br /> * по-умолчанию не указано<br /> *<br /> * sa.radius.connection.close.mode = 2<br /> * что делать для закрытия соединения:<br /> * 1 - ничего не делать<br /> * 2 - (default) посылаем PoD<br /> * 3 - посылаем subscriber:command=account-logoff<br /> *<br /> * sa.radius.connection.close.disableServices = 0<br /> * отключать ли сервисы ISG при закрытии<br /> * 0 - (default) не отключать<br /> * 1 - отключать (посылаем CoA на отключение всех сервисов перед тем как закрыть соединение по sa.radius.connection.close.mode)<br /> */<br /> public class ISGServiceActivator<br /> extends AbstractRadiusServiceActivator<br /> implements ServiceActivator<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGServiceActivator.class );<br /> <br /> /**<br /> * per-realm:<br /> * код опции -&gt; набор сервисов ISG<br /> */<br /> protected Map&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt; optionISGServiceMap = new HashMap&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt;();<br /> <br /> /**<br /> * имя(имена) сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND_PACKET = 0;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND = 1;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> */<br /> protected static final int COA_MODE_SUBSCR_COMMAND = 2;<br /> <br /> /**<br /> * Режим отправки команд<br /> */<br /> protected int coaMode;<br /> <br /> @Deprecated<br /> protected static final int CLOSE_MODE_POD_DEPRECATED = 0;<br /> <br /> protected static final int CLOSE_MODE_NONE = 1;<br /> protected static final int CLOSE_MODE_POD = 2;<br /> protected static final int CLOSE_MODE_SUBSCR_COMMAND = 3;<br /> <br /> protected int closeMode;<br /> protected boolean disableServicesOnClose;<br /> <br /> public ISGServiceActivator()<br /> {<br /> super( null, false, &quot;Acct-Session-Id&quot;, false );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object init( Setup setup, int moduleId, InetDevice device, InetDeviceType deviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, device, deviceType, deviceConfig );<br /> <br /> this.coaMode = deviceConfig.getInt( &quot;sa.radius.connection.coa.mode&quot;, deviceConfig.getInt( &quot;radius.coa.mode&quot;, deviceConfig.getInt( &quot;coa.mode&quot;, COA_MODE_SSG_COMMAND ) ) );<br /> <br /> //вендор атрибута cisco-SSG-Account-Info (9)<br /> int ciscoSSGAccountInfo_attribute_vendor=9;<br /> //id атрибута cisco-SSG-Account-Info (250)<br /> int ciscoSSGAccountInfo_attribute_id=250;<br /> <br /> Map&lt;Integer, Set&lt;String&gt;&gt; map;<br /> Set&lt;String&gt; set;<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; raList;<br /> InetOptionRuntimeMap inetOptionRuntimeMap = InetOptionRuntimeMap.getInstance(moduleId);<br /> // определение сервисов на каждой из опций<br /> for(Map.Entry&lt;String, Map&lt;Integer, RadiusAttributeSet&gt;&gt; e_realm : this.optionRadiusAttributesMap.getRealmMap().entrySet()){<br /> <br /> map = this.optionISGServiceMap.get(e_realm.getKey());<br /> if(null==map){<br /> map = new HashMap&lt;Integer, Set&lt;String&gt;&gt;();<br /> this.optionISGServiceMap.put(e_realm.getKey(), map);<br /> }<br /> //Перебираем опции в realm-е<br /> for(Map.Entry&lt;Integer, RadiusAttributeSet&gt; e_option : e_realm.getValue().entrySet()){<br /> logger.info(&quot;option = &quot;+inetOptionRuntimeMap.get(e_option.getKey()).title+&quot;(&quot;+e_option.getKey()+&quot;), realm = &quot;+e_realm.getKey()+&quot;, ra = &quot;+e_option.getValue());<br /> set = null;<br /> raList = e_option.getValue().getAttributes(ciscoSSGAccountInfo_attribute_vendor, ciscoSSGAccountInfo_attribute_id);<br /> if(raList!=null){<br /> for(RadiusAttribute&lt;?&gt; attr : raList){<br /> if(null==set){<br /> set = new HashSet&lt;String&gt;();<br /> }<br /> //вырезаем из атрибута cisco-SSG-Account-Info=ASERVICENAME имя сервиса SERVICENAME<br /> set.add(attr.getValue().toString().substring(1));<br /> }<br /> if(set!=null &amp;&amp; set.size()&gt;0){<br /> map.put(e_option.getKey(), set);<br /> }<br /> }<br /> }<br /> }<br /> <br /> // сервис(ы), отправляемый в режиме Reject-To-Accept<br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;sa.radius.service.disable&quot;, deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> <br /> logger.info( &quot;Disable services: &quot; + disableServiceNames );<br /> <br /> this.closeMode = deviceConfig.getInt( &quot;sa.radius.connection.close.mode&quot;, CLOSE_MODE_POD );<br /> this.disableServicesOnClose = deviceConfig.getInt( &quot;sa.radius.connection.close.disableServices&quot;, 0 ) &gt; 0;<br /> <br /> return null;<br /> }<br /> <br /> /**<br /> *<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionModify( ServiceActivatorEvent e )//TODO добавить timeout, чтобы не отправлять слишком быстро. Дожидаться ответов например.<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection modify: oldState: &quot; + e.getOldState() + &quot;; newState: &quot; + e.getNewState() + &quot;; oldOptionSet: &quot; + e.getOldOptions() + &quot;; newOptionSet: &quot; + e.getNewOptions() );<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( e.getNewState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> <br /> if( e.getOldState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> // отключаем disable сервис и включаем активные опции<br /> return sendCommands( connection, disableServiceNames, optionsToServiceNames(e.getRealm(), e.getNewOptions()) );<br /> }<br /> <br /> Collection&lt;Integer&gt; removeOptions = e.getOptionsToRemove();<br /> Collection&lt;Integer&gt; addOptions = e.getOptionsToAdd();<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), removeOptions), optionsToServiceNames(e.getRealm(), addOptions ) );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionClose( ServiceActivatorEvent e )<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection close&quot; );<br /> <br /> Object result;<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( disableServicesOnClose )<br /> {<br /> result = sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> else<br /> {<br /> result = null;<br /> }<br /> <br /> switch( closeMode )<br /> {<br /> default:<br /> case CLOSE_MODE_NONE:<br /> {<br /> break;<br /> }<br /> <br /> case CLOSE_MODE_POD_DEPRECATED:<br /> case CLOSE_MODE_POD:<br /> {<br /> RadiusPacket request = radiusClient.createDisconnectRequest();<br /> prepareRequest( request, connection );<br /> <br /> logger.info( &quot;Send PoD: \n&quot; + request );<br /> result = radiusClient.sendAsync( request );<br /> <br /> break;<br /> }<br /> <br /> case CLOSE_MODE_SUBSCR_COMMAND:<br /> {<br /> logger.info( &quot;Connection close (logoff)&quot; );<br /> <br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=account-logoff&quot; ) );<br /> <br /> logger.info( &quot;Send logoff CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected Collection&lt;String&gt; optionsToServiceNames(String realm, final Collection&lt;Integer&gt; options)//, final Collection&lt;String&gt; serviceNames )<br /> {<br /> if( options == null || options.size() == 0 )<br /> {<br /> return null;<br /> }<br /> <br /> if(null==realm || &quot;&quot;.equals(realm)){<br /> realm = &quot;default&quot;;<br /> }<br /> <br /> final Set&lt;String&gt; result = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;( options.size() + 2 ) );<br /> <br /> for( Integer option : options )<br /> {<br /> Set&lt;String&gt; serviceNames = this.optionISGServiceMap.get(realm).get(option);<br /> if( serviceNames == null ){<br /> serviceNames = this.optionISGServiceMap.get(&quot;default&quot;).get(option);<br /> }<br /> if( serviceNames != null )<br /> {<br /> result.addAll( serviceNames );<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> /**<br /> * Отправка команд на деактивацию и активацию сервисов<br /> * @param connection - InetConnection<br /> * @param serviceNamesDeactivate - список сервисов, которые нужно деактивировать<br /> * @param serviceNamesActivate - список сервисов, которые нужно активировать<br /> * @return<br /> * @throws Exception<br /> */<br /> protected Object sendCommands( final InetConnection connection, final Collection&lt;String&gt; serviceNamesDeactivate, final Collection&lt;String&gt; serviceNamesActivate )<br /> throws Exception<br /> {<br /> Object result = null;<br /> <br /> if(logger.isInfoEnabled()){<br /> logger.info(&quot;Sending commands to deactivate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesDeactivate)+&quot;]&quot;);<br /> logger.info(&quot;Sending commands to activate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesActivate)+&quot;]&quot;);<br /> }<br /> <br /> switch( coaMode )<br /> {<br /> case COA_MODE_SSG_COMMAND_PACKET:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send deactivate services CoA:\n&quot; + packet );<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send activate services CoA:\n&quot; + packet );<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SSG_COMMAND:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SUBSCR_COMMAND:<br /> default:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=deactivate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=activate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> ==== ProtocolHandler ====<br /> Используется собственный ProtocolHandler, необходимый для поиска сервисов Inet по порту на основе Nas-Port-Id.<br /> Задача в том, чтобы по данным из радиус-пакета авторизации найти и авторизовать сервис в биллинге.<br /> В пакете приходят:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> User-Name=nas-port:XXX.XXX.XXX.XXX:0/0/0/1112<br /> NAS-Port-Id=0/0/0/1112<br /> &lt;/source&gt;<br /> где <br /> XXX.XXX.XXX.XXX - ip-адрес NAS-а<br /> 0/0/0/1112 - слот/карта/интерфейс/инкапсуляция<br /> Было решено использовать NAS-Port-Id<br /> <br /> Схема следующая:<br /> *При подключении клиента конфигурируем имя интерфейса в соответствии с инкапсуляцией: для 'encapsulation dot1Q XXX' интерфейс будет 'interface GigabitEthernet0/0.XXX'<br /> *Заводим интерфейс на устройстве в биллинге: имя=Gi0/0.XXX. ''При необходимости проставляем нужный ifIndex в поле 'интерфейс', если хотим собирать netflow''<br /> *При загрузке (или перечитывании конфигурации) наш ISGIPoEProtocolHandler парсит параметры конфигурации radius.ipoe.nas_port_id.pattern.*, по которым создаёт в памяти соответствие 'NAS-Port-Id'-&gt;'ifaceId' для каждого заведённого в биллинге интерфейса устройства. Например, '0/0/0/1112'-&gt;'id интерфейса с именем Gi0/0.1112 в биллинге'<br /> *При авторизации ISGIPoEProtocolHandler ищет интерфейс по NAS-Port-Id из пакета и проставляет опцию INTERFACE_ID, по которой стандартный радиус-процессор будет затем искать сервис Inet.<br /> *При изменении интерфейсов устройства в биллинге кэш 'NAS-Port-Id'-&gt;'ifaceId' в ISGIPoEProtocolHandler-е автоматически обновляется<br /> <br /> ''Примечание:<br /> Если используется QinQ, то NAS-Port-Id будет вида 0/0/0/105.2015 для: 'encapsulation dot1Q 2015 second-dot1q 105', а имя интерфейса: 'Gi0/0.20150105'. Последнее настраивается в radius.ipoe.nas_port_id.pattern.*''<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.managed.ServerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.bitel.oss.systems.inventory.resource.common.DeviceInterfaceService;<br /> import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterface;<br /> import ru.bitel.oss.systems.inventory.resource.common.event.DeviceInterfaceModifiedEvent;<br /> <br /> import java.util.HashMap;<br /> import java.util.List;<br /> import java.util.Map;<br /> import java.util.SortedMap;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * ProtocolHandler для работы с cisco ip subscriber interface + cisco ISG<br /> * В предобработке устанавливается опция пакета InetRadiusProcessor.INTERFACE_ID, где указывается номер интерфейса в биллинге,<br /> * соответствующий атрибуту Nas-Port-Id из пакета<br /> * Соответствие определяется по шаблонам, заданным в конфигурации, и имени интерфейса сервиса в биллинге<br /> * Предполагается, что клиент авторизуется именно на том устройстве, на котором указан этот ProtocolHandler (не на дочерних)<br /> *<br /> * Параметры конфигурации:<br /> * radius.ipoe.nas_port_id.pattern.[i].pattern - регэксп-шаблон для имени интерфейса в биллинге<br /> * radius.ipoe.nas_port_id.pattern.[i].replacement - выражение для построения nas_port_id по шаблону<br /> *<br /> * Пример:<br /> * Gi0/0.1112 -&gt; 0/0/0/1112<br /> * Gi0/0.20150105 -&gt; 0/0/0/105.2015<br /> *<br /> */<br /> public class ISGIPoEProtocolHandler extends ISGProtocolHandler implements RadiusProtocolHandler {<br /> <br /> private static final Logger logger = Logger.getLogger( ISGIPoEProtocolHandler.class );<br /> <br /> /**<br /> * Кэш интерфейсов устройства<br /> */<br /> private volatile DeviceNasPortMap ifaceMap;<br /> <br /> @Override<br /> public void init(Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig) throws Exception {<br /> super.init(setup, moduleId, inetDevice, inetDeviceType, deviceConfig);<br /> this.ifaceMap = new DeviceNasPortMap(moduleId, inetDevice.getId(), deviceConfig.subIndexed(&quot;radius.ipoe.nas_port_id.pattern.&quot;));<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public void preprocessAccessRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccessRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета InetRadiusProcessor.INTERFACE_ID<br /> * @param request радиус-пакет<br /> */<br /> private void setBGIfaceId(RadiusPacket request) {<br /> String nas_port_id = request.getStringAttribute(-1, RadiusDictionary.NAS_Port_Id, null);<br /> Integer port=-1;<br /> if(nas_port_id!=null){<br /> port = this.ifaceMap.getIfacePort(nas_port_id);<br /> }<br /> if(null==port)<br /> {<br /> port=-1;//Насчёт port=0 и port=-1 - см http://forum.bgbilling.ru/viewtopic.php?f=44&amp;t=7694&amp;p=64541#p64541<br /> }<br /> request.setOption(InetRadiusProcessor.INTERFACE_ID, port);<br /> }<br /> <br /> /**<br /> * Кэш соответствий Nas-Port-Id -&gt; id интерфейса в биллинге для устройства<br /> * Обновляется при изменении порта или перезагрузке конфигурации<br /> */<br /> private class DeviceNasPortMap implements EventListener&lt;DeviceInterfaceModifiedEvent&gt; {<br /> /**<br /> * Соответствие cisco Nas-Port-Id -&gt; id интерфейса в биллинге<br /> */<br /> private volatile Map&lt;String, Integer&gt; nasPortIdToBGPortIdMap;<br /> private final int moduleId;<br /> private final int deviceId;<br /> /**<br /> * Список шаблонов-регулярных выражений, по которым будем получать Nas-Port-Id по названию интерфейса<br /> * Список паттернов не обновляется, т.к. берётся из конфига.<br /> * При перезагрузке конфига в любом случае ISGIPoEProtocolHandler будет переинициализирован целиком<br /> */<br /> private final SortedMap&lt;Integer, ParameterMap&gt; patternMap;<br /> <br /> public DeviceNasPortMap(int moduleId, int deviceId, SortedMap&lt;Integer, ParameterMap&gt; patternMap) throws BGException {<br /> this.moduleId = moduleId;<br /> this.deviceId = deviceId;<br /> this.patternMap = patternMap;<br /> EventProcessor.getInstance().addListener(this, DeviceInterfaceModifiedEvent.class);<br /> this.load();<br /> }<br /> <br /> private synchronized void load(){<br /> this.nasPortIdToBGPortIdMap = new HashMap&lt;String, Integer&gt;();<br /> //Перебираем порты устройств<br /> logger.info(&quot;(Re)loading DeviceNasPortMap for device &quot;+this.deviceId);<br /> ServerContext ctx = (ServerContext) ThreadContext.get();<br /> try {<br /> DeviceInterfaceService devicePortService = ctx.getService(DeviceInterfaceService.class, moduleId);<br /> List&lt;DeviceInterface&gt; deviceIfaceList = devicePortService.devicePortList(this.deviceId);<br /> String nasPortId;<br /> if(deviceIfaceList!=null){<br /> for(DeviceInterface iface : deviceIfaceList){<br /> nasPortId = nasPortIdByIfaceTitle(iface.getTitle());<br /> if(null!=nasPortId){<br /> nasPortIdToBGPortIdMap.put(nasPortId, iface.getPort());<br /> logger.debug(&quot;[device id=&quot; + this.deviceId + &quot;]: nas-port-id='&quot; + nasPortId + &quot;' -&gt; &quot; + iface.getPort());<br /> }<br /> }<br /> }<br /> } catch (BGException e) {<br /> logger.error(&quot;Error (re)loading DeviceNasPortMap&quot;, e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает Nas-Port-Id по имени интерфейса на основе регекспов из patternMap<br /> * @param ifaceTitle имя инерфейса (ex Gi0/0.123)<br /> * @return Nas-Port-Id (ex 0/0/0/123)<br /> */<br /> protected String nasPortIdByIfaceTitle(String ifaceTitle){<br /> if(null==ifaceTitle){<br /> return null;<br /> }<br /> Pattern p;<br /> Matcher m;<br /> String pattern;<br /> String replacement;<br /> String nasPortId;<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; patternMapEntry : patternMap.entrySet()){<br /> pattern = patternMapEntry.getValue().get(&quot;pattern&quot;, null);<br /> replacement = patternMapEntry.getValue().get(&quot;replacement&quot;, null);<br /> if(pattern!=null &amp;&amp; replacement!=null){<br /> p = Pattern.compile(pattern);<br /> m = p.matcher(ifaceTitle);<br /> if (m.find()) {<br /> //Получаем логин путём подстановки найденных capturing groups в $1, $2 и т.д. шаблона<br /> nasPortId = m.replaceFirst(replacement);<br /> return nasPortId;<br /> }<br /> }<br /> }<br /> return null;<br /> }<br /> <br /> /**<br /> * Получаем id порта в биллинге по nas_port_id из кэша<br /> */<br /> public Integer getIfacePort(String nas_port_id) {<br /> return this.nasPortIdToBGPortIdMap.get(nas_port_id);<br /> }<br /> <br /> /**<br /> * Обновляем кэш при изменении интерфейса<br /> * @throws BGException<br /> */<br /> @Override<br /> public void notify(DeviceInterfaceModifiedEvent event, EventListenerContext eventListenerContext) throws BGException {<br /> DeviceInterface deviceIface = event.getNewItem();<br /> if(deviceIface==null){<br /> deviceIface = event.getOldItem();<br /> }<br /> if(deviceIface!=null){<br /> if(deviceIface.getDeviceId()==this.deviceId){<br /> this.load();<br /> }<br /> }<br /> }<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.dhcp.DhcpProtocolHandler;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> <br /> import java.util.Collections;<br /> import java.util.LinkedHashMap;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * Базовый класс для Cisco ISG<br /> * Копипаста бителовского, без лишней потехи с option 82<br /> */<br /> public class ISGProtocolHandler<br /> extends AbstractRadiusProtocolHandler<br /> implements RadiusProtocolHandler, DhcpProtocolHandler<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGProtocolHandler.class );<br /> <br /> /**<br /> * Код атрибута - id родительского аккаунтинга<br /> */<br /> protected int parentAcctSessionIdType;<br /> <br /> /**<br /> * Префикс id родительского аккаунтинга<br /> */<br /> protected String parentAcctSessionIdPrefix;<br /> <br /> /**<br /> * Код атрибута - имя сервиса (для cisco-avpair)<br /> */<br /> protected int serviceNameType;<br /> <br /> /**<br /> * Префикс имени сервиса (для cisco-avpair)<br /> */<br /> protected String serviceNamePrefix;<br /> <br /> /**<br /> * Имя сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> public ISGProtocolHandler()<br /> {<br /> super( 9 ); // Cisco<br /> }<br /> <br /> @Override<br /> public void init( Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, inetDevice, inetDeviceType, deviceConfig );<br /> <br /> parentAcctSessionIdType = deviceConfig.getInt( &quot;radius.parentAcctSessionId.type&quot;, 1 ); // cisco-avpair<br /> parentAcctSessionIdPrefix = deviceConfig.get( &quot;radius.parentAcctSessionId.prefix&quot;, &quot;parent-session-id=&quot; );<br /> serviceNameType = deviceConfig.getInt( &quot;radius.serviceName.type&quot;, 251 ); // cisco-SSG-Service-Info<br /> serviceNamePrefix = deviceConfig.get( &quot;radius.serviceName.prefix&quot;, &quot;&quot; );<br /> <br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )<br /> throws Exception<br /> {<br /> int acctStatusType = request.getIntAttribute( -1, RadiusDictionary.Acct_Status_Type, -1 );<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> <br /> // извлекаем parentAcctSessionId<br /> String parentAcctSessionId;<br /> // если parentAcctSessionId находится в cisco-avpair - то нужно искать по префиксу<br /> if( parentAcctSessionIdType == 1 )<br /> {<br /> parentAcctSessionId = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; attributes = request.getAttributes( radiusVendor, parentAcctSessionIdType );<br /> if( attributes != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; attr : attributes )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> String value = ((RadiusAttribute&lt;String&gt;)attr).getValue();<br /> if( value.startsWith( parentAcctSessionIdPrefix ) )<br /> {<br /> parentAcctSessionId = value.substring( parentAcctSessionIdPrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> parentAcctSessionId = request.getStringAttribute( radiusVendor, parentAcctSessionIdType, null );<br /> }<br /> <br /> // если это аккаунтинг сервисной сессии<br /> if( parentAcctSessionId != null )<br /> {<br /> // извлекаем serviceName<br /> String serviceName;<br /> // если serviceName находится в cisco-avpair - то нужно искать по префиксу<br /> if( serviceNameType == 1 )<br /> {<br /> serviceName = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> final List&lt;RadiusAttribute&lt;?&gt;&gt; ras = request.getAttributes( radiusVendor, serviceNameType );<br /> if( ras != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; ra : ras )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> final String value = ((RadiusAttribute&lt;String&gt;)ra).getValue();<br /> if( value.startsWith( serviceNamePrefix ) )<br /> {<br /> serviceName = value.substring( serviceNamePrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> serviceName = request.getStringAttribute( radiusVendor, serviceNameType, null );<br /> }<br /> <br /> if( serviceName == null || !serviceName.startsWith( &quot;N&quot; ) )<br /> {<br /> logger.error( &quot;Parent acctSessionId found, but ServiceName is not&quot; );<br /> }<br /> else<br /> {<br /> serviceName = serviceName.substring( 1 );<br /> }<br /> <br /> // устанавливаем id родительской сессии<br /> request.setOption( InetRadiusProcessor.PARENT_ACCT_SESSION_ID, parentAcctSessionId );<br /> // устанавливаем имя сервиса текущего аккаунтинга<br /> request.setOption( InetRadiusProcessor.SERVICE_NAME, serviceName );<br /> <br /> // если указан сервис, при котором доступ ограничен - проверяем, не его ли это аккаунтинг,<br /> // и, если это так, переключаем состояние соединения<br /> if( disableServiceNames != null &amp;&amp; disableServiceNames.contains( serviceName ) )<br /> {<br /> // start или update<br /> if( acctStatusType == 1 || acctStatusType == 3 )<br /> {<br /> logger.debug( &quot;State is disable (from start disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_DISABLE );<br /> }<br /> else<br /> {<br /> logger.debug( &quot;State is enable (from stop disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_ENABLE );<br /> }<br /> }<br /> }<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> === Устройства ===<br /> Дерево устройств:<br /> <br /> [[Файл:devices.png]]<br /> <br /> Интерфейсы:<br /> <br /> [[Файл:device-ifaces.png]]<br /> <br /> === IP-ресурсы ===<br /> Не используются.<br /> === VLAN-ресурсы ===<br /> Не используются.<br /> <br /> == Настройка BGInetAccess и BGInetAccounting ==<br /> <br /> === BGInetAccess-VPN ===<br /> <br /> '''access.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3851<br /> MEMORY=-Xmx512m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-access.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;4&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;3&quot;/&gt;<br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3812&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> === BGInetAccess-ISG ===<br /> '''TODO''': описать настройку справочника сервисов ISG.<br /> <br /> У нас для этих целей сейчас используется модуль Dialup - так исторически сложилось.<br /> Но по-хорошему, нужно настроить отдельный BGInetAccess, повешать его на отдельную ProcessGroup и авторизовать сервисы из него со специального служебного договора.<br /> Пример есть в статье [[ISG,_схема_со_стартом_сессии_и_ее_авторизацией_по_IP,_выдача_адресов_на_основе_option82_(Конфигурация_BGBilling%27а)]] (см. 'ASR ISG Service')<br /> <br /> === BGInetAccounting-VPN ===<br /> <br /> '''accounting.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccounting-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-accounting.xml&quot;<br /> NAME=inet-accounting<br /> NAME_SHORT=accounting<br /> ADMIN_PORT=3852<br /> MEMORY=-Xmx1024m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &quot;$@&quot;<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-accounting.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;accounting&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccounting-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;5&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения radius-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Параметры сохранения flow-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.flow.dir&quot; value=&quot;data/flow&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл и поток слушателя --&gt;<br /> &lt;param name=&quot;datalog.flow.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.flow.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Создание Accounting --&gt;<br /> &lt;bean name=&quot;accounting&quot; class=&quot;ru.bitel.bgbilling.modules.inet.accounting.Accounting&quot;/&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3813&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;1 * 1024 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;30&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;500&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.accounting --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.accounting&lt;/param&gt;<br /> &lt;!-- Передача setup --&gt;<br /> &lt;param name=&quot;setup&quot;&gt;setup&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> == Тарифы ==<br /> <br /> Безлимит 1Мбит:<br /> <br /> [[Файл:vpn-unlim.png]]<br /> <br /> = Конфигурация на cisco =<br /> <br /> Пример конфига интерфейса клиента:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> interface GigabitEthernet0/0.127<br /> description ---- TEST VPN ----<br /> encapsulation dot1Q 127<br /> ip vrf forwarding vpn-test<br /> ip address IP.IP.IP.IP MASK.MASK.MASK.MASK<br /> no ip redirects<br /> no ip proxy-arp<br /> ip verify unicast source reachable-via rx<br /> no cdp enable<br /> service-policy type control IP-ISG-VRFSUB<br /> ip subscriber interface<br /> end<br /> &lt;/source&gt;<br /> <br /> ...<br /> <br /> = Дополнительно =<br /> *[[Мониторинг_Inet-Radius_через_JMX]]<br /> * Последние версии кода: [https://github.com/Cromeshnic/BGTools/ на github]<br /> *[[Справочник_Cisco-ISG_сервисов]]<br /> <br /> = TODO =<br /> *Описать настройку [[http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 справочника сервисов ISG]] (Done)<br /> *Выложить дин. код на github<br /> *Описать подключение netflow для детализации<br /> *Скрипт обновления/проставления ifindex на интерфейсах в BG<br /> *Автоконфигурирование интерфейса по telnet/ssh при создании/удалении<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 05:11, 25 июля 2013 (UTC)</div> Mon, 21 Jul 2014 06:36:10 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG Vlan per user + Cisco IP subscriber interface + ISG http://wiki.bitel.ru/index.php/Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG <p>Cromeshnic:&#32;/* TODO */</p> <hr /> <div>= Описание задачи =<br /> Клиентам предоставляется доступ по схеме vlan per user на роутерах cisco.<br /> Необходимо реализовать учёт и управление услугами в модуле Inet.<br /> <br /> = Решение =<br /> Сразу оговоримся, что в нашем случае речь идёт об услуге '''MPLS IP VPN'''.<br /> Для доступа интернет, возможно, появятся какие-то нюансы, но в целом механизм тот же.<br /> <br /> Схема решения такая:<br /> *на клиентском интерфейсе настраивается ip subscriber interface + isg<br /> *интерфейсы авторизуются в BGInetAccess по паре &quot;устройство+интерфейс&quot;<br /> *Настройки скорости выдаются через ISG-сервисы, смена тарифа происходит через CoA<br /> *Трафик собираем по радиус-счётчикам ISG-сервисов и родительской сессии, либо через Netflow<br /> *В сервисе модуля Inet указываем только устройство и интерфейс<br /> <br /> В нашем случае в биллинге не задаются ни сеть IP клиента на интерфейсе, ни его VLAN, т.к. для тарификации они не играют роли.<br /> Но это можно реализовать для учёта ресурсов, а также для автоматического конфигурирования.<br /> <br /> ''Дополнительно усложним задачу:<br /> В некоторых случаях клиенту предоставляется VPN + доступ в интернет через NAT на нашей циске.<br /> Доступ в интернет считается в другом модуле, но такая схема несколько усложняет задачу, поскольку тогда в каждой точке VPN-а нужно разделять трафик на собственно VPN и интернет, чтобы не тарифицировать последний 2 раза. Эту схему мы реализуем отдельным типом сервиса с отдельной привязкой трафика и отдельными сервисами ISG.''<br /> <br /> = Настройка биллинга =<br /> == Настройка модуля ==<br /> Для услуг VPN было решено завести отдельный экземпляр модуля Inet с названием 'VPN'.<br /> <br /> Плюсы:<br /> *Можно учитывать услуги интернета и vpn на одном договоре без проблем с пересечением тарифных планов (основная причина, т.к. в наследство осталось много таких договоров)<br /> *Учёт услуг, визуальное разделение услуг на договоре и в отчётах.<br /> <br /> Минусы:<br /> *Дублирование ресурсов между несколькими экземплярами модулей Inet: одни и те же устройства, vlan, интерфейсы используются в разных местах. Могут быть проблемы с учётом.<br /> <br /> Конфигурация модуля:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Активные и приостановленные статусы договора<br /> contract.status.active.codes=0<br /> contract.status.suspend.codes=3,4<br /> # Проверка цены в тарифе: 0 - проверка отсутсвует, 1 - ошибка, только если у сессии есть трафик определенного типа,<br /> # но для него нет цены, 2 - ошибка, если хотя бы для одного типа трафика в привязке типа сервиса нет цены (по умолчанию - 1)<br /> #http://forum.bgbilling.ru/viewtopic.php?p=65629#p65629<br /> accounting.tariffication.checkPrice=0<br /> # Режим активации учетного периода, если не используется скрипт на событие активации,<br /> # 0 (по умолчанию) - активация со дня подключения (старта сессии), 1 - активация с начала месяца.<br /> # Следует учитывать, что учетный период является второй величиной при вычислении пропорциональности<br /> # в тарифной ветке &quot;Диапазон трафика&quot;<br /> #accounting.period.activation.mode=0<br /> <br /> # Нужно ли отключать сервис с типом инициации &quot;по трафику&quot;, если тариф не найден<br /> #serv.disableOnTariffError=0<br /> <br /> #Пункты Web - меню<br /> #web.menuItem1=Отчет по сессиям Inet<br /> #web.menuItem2=Смена пароля на логины Inet<br /> #web.menuItem3=none<br /> #web.menuItem3=Отчет по трафикам Inet<br /> <br /> # Параметры автоматической генерации логина для сервиса.<br /> # Минимальное значение логина при генерации логина<br /> #serv.login.min=1<br /> # Максимальное значение логина при генерации логина (т.е. если в базе присутствуют логины 1,2,3 и 10000000,<br /> # то при генерации создастся логин 4, а не 10000001)<br /> #serv.login.max=9999999<br /> <br /> # Парамерты автоматической генерации пароля для сервиса. Можно указать в конфигурации модуля, конфигурации устройства, конфигурации типа сервиса<br /> # (в последнем случае значения будут главнее):<br /> # Минимальная длина пароля<br /> serv.password.length.min=5<br /> # Максимальная длина пароля<br /> serv.password.length.max=16<br /> # Разрешенные символы (используются также при генерации пароля)<br /> serv.password.chars=1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz<br /> # Описание разрешенных символов, если пользователь ввел другие<br /> serv.password.chars.description=В пароле допустимы только цифры и латинские буквы.<br /> # Длина для автоматически генерируемого пароля<br /> serv.password.length.auto=6<br /> # Используемые символы для автоматически генерируемого пароля (по умолчанию значение берется из параметра serv.password.chars)<br /> #serv.password.chars.auto=<br /> <br /> # Параметры активации карточек модуля card при использовании InetRadiusProcessor,<br /> # данные параметры можно указать как в конфигурации модуля, так и в конфигурации устройства.<br /> # Код модуля card<br /> #card.moduleId=<br /> # id услуг активации<br /> #card.activate.serviceIds=<br /> # Минимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.min=0<br /> # Максимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.max=0<br /> &lt;/source&gt;<br /> <br /> Также не забываем настроить задачу планировщика &quot;Активация/деактивация сервисов по периоду&quot; хотя бы раз в сутки в полночь, чтобы корректно обрабатывалось переоформление и перенос сервисов с договора на договор будущим числом.<br /> <br /> == Типы трафиков и привязки ==<br /> === Типы трафиков ===<br /> <br /> [[Файл:traffic_types.png]]<br /> <br /> === Привязки ===<br /> ==== Radius - full ====<br /> Простая привязка с 2 типами трафика: входящий/исходящий, которые берутся из стандартных счетчиков радиус-пакетов родительской сессии.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> &lt;div class=&quot;collapsible collapsed&quot;&gt;[[Файл:radius-full.png]]&lt;/div&gt;<br /> <br /> ==== Radius - NAT ====<br /> Привязка для VPN + NAT.<br /> Для родительской сессии будет 2 дочерних сессии ISG: IPVPN-NAT-INET и IPVPN-NAT-VPN-xxx, где xxx - скорость.<br /> Будем брать трафики из них.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> [[Файл:radius-nat.png]]<br /> <br /> == Типы сервисов ==<br /> <br /> === VPN-IPoE ===<br /> [[Файл:serv-type-ipoe.png]]<br /> <br /> === VPN-IPoE (+NAT) ===<br /> [[Файл:serv-type-ipoe-nat.png]]<br /> <br /> == Опции ==<br /> Заведём опции для соответствующих ISG-сервисов.<br /> Опции FLOWON/FLOWOFF нужны для включения/выключения netflow на интерфейсе<br /> [[Файл:options.png]]<br /> <br /> == Устройства и ресурсы ==<br /> <br /> === Группы устройств ===<br /> Не используются.<br /> === Типы устройств ===<br /> Мы используем 3 типа устройств:<br /> *Группа (ProcessGroup) - пустой тип устройств. Указывается в качестве рута для BGInetAccess и BGInetAccounting (см соответствующий раздел)<br /> *Город - пустой тип устройства, добавлен для разбиения дерева по городам. В будущем, возможно, к нему будут привязываться специфические ProcessHandler-ы, отдельные конфиги или выделяться свои Access и Accounting сервера для каждого города.<br /> *IPoE - тип устройства для цисок с поддержкой ip subscriber interface + ISG<br /> <br /> [[Файл:devicetype-ipoe.png]]<br /> <br /> Конфиг типа устройства IPoE:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Realm default атрибуты<br /> radius.realm.default.attributes=cisco-SSG-Account-Info=ADEFAULT;cisco-avpair=subscriber:accounting-list=BG-DSI-IPVRF<br /> #коды ошибок, которые обрабатываются системой Reject-To-Accept (то же самое, что и realm.reject.error)<br /> #http://bgbilling.ru/v5.2/doc/ch18s20.html<br /> #-------reject-to-accept отсутствует<br /> #radius.disable.accessCodes=4,10,11,12,44<br /> # Какие адреса выдавать при ответе Access-Accept в состоянии disable: <br /> # 0 (по умолчанию) - из radius.disable.ipCategories, 1 - так же, как если бы не было ошибки (в том числе привязанные к сервису в договоре)<br /> #radius.disable.mode=0<br /> # код категории ресурсов Fake пула<br /> #radius.disable.ipCategories=7<br /> # радиус атрибуты, отправляемые в режиме Reject-To-Accept<br /> #radius.disable.attributes=<br /> # Id фиктивного сервиса, к которому будут привязываться сессии, по которым нормальный сервис не был найден (код ошибки: 1, логин не найден).<br /> # Необходим, если в radius.disable.accessCodes присутствует код 1<br /> #radius.disable.servId=<br /> # Атрибуты, при наличии которых соединение должно считаться в состоянии DISABLE (т.е. с ограниченным доступом)<br /> #radius.disable.pattern.attributes=<br /> # Вендор атрибута, где хранится MAC-адрес<br /> # Берём стандартный NAS-Port-Id<br /> radius.macAddress.vendor=-1<br /> # Код атрибута, где хранится MAC-адрес<br /> # Берём NAS-Port-Id<br /> radius.macAddress.type=87<br /> # Префикс атрибута (если есть), где хранится MAC-адрес. Например, для cisco avpair <br /> #radius.macAddress.prefix=<br /> #Порт для отправки PoD и CoA запросов (по умолчанию - порт, заданный в параметрах устройства Хост/порт)<br /> radius.port=1700<br /> #<br /> # Режим поиска сервиса: 0 (по умолчанию) - по логину, 1 - по интерфейсу на устройстве (в предобработке должны быть<br /> # проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или INTERFACE_ID), 2 - по VLAN на устройстве (в предобработке<br /> # должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID), 4 - по VLAN на устройстве или<br /> # дочернем устройстве (в предобработке должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID),<br /> # 5 - по MAC-адресу на устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS), 6 - по MAC-адресу на<br /> # устройстве или дочернем устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS).<br /> radius.servSearchMode=1,0<br /> #<br /> # Нужно ли проверять пароль: 0 - нет, 1 (по умолчанию) - да.<br /> radius.password.verification=0<br /> #<br /> # При выдаче access-accept добавлять запись в базу<br /> # необходимо, если используется reject-to-accept и по старт пакету нельзя определить в каком состоянии соединение<br /> #чтобы Access при Access-Accept добавлял соединение в базе со статусом WAIT и указанием выданного состояния и опций<br /> connection.start.fromAccept=1<br /> # Бывают ситуации, когда start-пакет не дошел до Accounting-сервера. В этом случае, при<br /> # 1 (значение по умолчанию) - сессия создастся от текущего момента,<br /> # 2 - Accounting проверит, что время сессии из update/stop пакета не больше, чем значение connection.close.timeout и создаст сессию от ее начала, иначе,<br /> # если время сессии больше чем connection.close.timeout, сессия создастся от текущего момента,<br /> # 0 - сессия без старт-пакета создана не будет.<br /> connection.start.fromUpdate=1<br /> # таймаут перевода соединения в статус suspended при остутствии радиус пакетов<br /> connection.suspend.timeout=1200<br /> # таймаут закрытия соединения при остутствии радиус пакетов (не складывается с connection.suspend.timeout)<br /> connection.close.timeout=1260<br /> #При завершении соединения по сигналу Stop-пакетом (RADIUS-Stop) оно фактически завершается через количество секунд, определяемое переменной connection.finish.timeout. Это позволяет, в частности, реализовать сбор &quot;запоздалой&quot; информации о трафике, которая может прийти после Stop-пакета.<br /> connection.finish.timeout=2<br /> # Проверка на повторную аутентификацию при Access-Request. Бывает нужна в случаях, когда NAS сбрасывает (теряет) сессию, но<br /> # Stop-пакет не присылает и клиент пытается подключиться повторно, но у него стоит ограничение на максимум одну сессию. При совпадении<br /> # callingStationId с одной из активных сессий и установленным параметром: 1 - осуществляется попытка закрытия старой сессии (connectionClose),<br /> # 2 - попытка закрытия сессии (connectionClose) и завершение ее в базе, не дожидаясь стоп пакета, 3 - завершение в базе.<br /> #radius.connection.checkDuplicate=0<br /> #<br /> # Нужно ли убирать домен перед поиском сервиса по логину из поля User-Name. По умолчанию - да (1).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> #для IPoE какой пришёл, такой и берём, там не должно быть лишнего<br /> radius.username.removeDomain=0<br /> #<br /> # Нужно ли убирать пробелы из поля User-Name перед поиском логина. По умолчанию - нет (0).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> radius.username.removeWhitespace=0<br /> #<br /> # Шаблон вывода ошибки в мониторе с использованием атрибутов из RADIUS-пакета<br /> #radius.accessError.infoPattern=LOGIN:$User-Name<br /> #<br /> # Параметры активации сервисов<br /> # длина паузы, если возникла ошибка<br /> #sa.error.pause=60<br /> # количество заданий за раз<br /> #sa.batch.size=20<br /> # время (сек) ожидания завершения всех заданий (при асинхронной работе)<br /> #sa.batch.wait=5<br /> # пауза (сек) после обработки заданий<br /> #sa.batch.pause=0<br /> # время (сек) ожидания новой задачи перед вызовом disconnect.<br /> #sa.batch.waitNext=5<br /> #<br /> #----------------------------------------<br /> #параметры обработчика активации сервисов<br /> #----------------------------------------<br /> # откуда при отправке CoA брать атрибуты опций (по умолчанию - те же атрибуты, что выдаются при удачной авторизации)<br /> #sa.radius.option.attributesPrefix=radius.inetOption.<br /> #sa.radius.connection.attributes=NAS-Port, Acct-Session-Id, User-Name, Framed-IP-Address, NAS-IP-Address, NAS-Identifier<br /> sa.radius.connection.attributes=Acct-Session-Id, User-Name<br /> #режим отправки CoA. 0 - команды 0xc и 0xb в одном пакете для всех сервисов, 1 - команды 0xc и 0xb в отдельном пакете для каждого сервиса, 2 - атрибуты subscriber:command= в раздельных пакетах для каждого сервиса<br /> sa.radius.connection.coa.mode=1<br /> #Что делать для закрытия соединения:<br /> # 0 (default) - ничего<br /> # 2 - шлём PoD<br /> # 3 - шлём subscriber:command=account-logoff<br /> sa.radius.connection.close.mode=3<br /> #если dhcp lease time большой, а при положительном балансе доступ нужно дать (даже если адрес сейчас выдан серый), нужно установить 1<br /> sa.radius.connection.coa.onEnable=0<br /> #атрибуты CoA запроса для прекращения доступа (используется при sa.radius.connection.withoutBreak=1)<br /> #sa.radius.disable.attributes={@radius.disable.attributes}<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой CoA<br /> #sa.radius.coa.attributes=<br /> #добавлять ли при отправке CoA атрибуты реалма (для default - из radius.realm.default.attributes)<br /> #sa.radius.realm.addAttributes=0<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой PoD<br /> #sa.radius.pod.attributes=<br /> #<br /> #<br /> ###### VPN services ######<br /> radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle<br /> #Сопоставление nas-port-id из запроса с id порта в биллинге по имени интерфейса (см ru.dsi.bgbilling.modules.inet.dyn.device.cisco.ISGIPoEProtocolHandler)<br /> radius.ipoe.nas_port_id.pattern.1.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{1,4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.1.replacement=$1/0/$2/$3<br /> radius.ipoe.nas_port_id.pattern.2.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{8}$)0{0,3}([1-9]\d{0,3})(?=\d{4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.2.replacement=$1/0/$2/$4.$3<br /> radius.ipoe.nas_port_id.pattern.3.pattern=^BD(\d+)$<br /> radius.ipoe.nas_port_id.pattern.3.replacement=255/0/$1<br /> &lt;/source&gt;<br /> <br /> Обратите внимание на строчку:<br /> &lt;source lang=&quot;ini&quot;&gt;radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle&lt;/source&gt;<br /> Здесь мы говорим, что все опции модуля, находящиеся в ветке с id=1 (&quot;Группа: VPN&quot;), следует трактовать как сервисы ISG с соответствующим названием.<br /> <br /> Параметры radius.ipoe.nas_port_id.pattern.* нужны для самописного предобработчика радиус-пакетов ISGIPoEProtocolHandler.<br /> В них определяются regexp-шаблоны, по которым сопоставляется значение радиус-атрибута Nas-Port-Id (например, 0/0/0/1112) и название интерфейса в биллинге (Gi0/0.1112).<br /> Подробнее см. описание класса.<br /> <br /> ==== ServciceActivator ====<br /> Для нашей схемы используется модифицированный ISGServciceActivator.<br /> <br /> Отличия от стандартного Бителовского:<br /> - соответствие &quot;опция - сервис ISG&quot; берётся из optionRadiusAttributesMap, чтобы работали новые шаблоны атрибутов (radius.inetOption.1.template)<br /> - в соответствие &quot;опция - сервис ISG&quot; добавлена зависимость от realm-а<br /> - убрано всё, что касается DHCP<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorEvent;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetConnection;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntimeMap;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> <br /> import java.util.*;<br /> <br /> /**<br /> * Конфигурация устройства:<br /> * sa.radius.connection.coa.mode = 1<br /> * режим отправки CoA:<br /> * 0 - отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> * 1 - (default) отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> * 2 - отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> *<br /> * sa.radius.service.disable =<br /> * имена сервисов, при котором доступ отключен<br /> * отправляются в режиме Reject-To-Accept<br /> * по-умолчанию не указано<br /> *<br /> * sa.radius.connection.close.mode = 2<br /> * что делать для закрытия соединения:<br /> * 1 - ничего не делать<br /> * 2 - (default) посылаем PoD<br /> * 3 - посылаем subscriber:command=account-logoff<br /> *<br /> * sa.radius.connection.close.disableServices = 0<br /> * отключать ли сервисы ISG при закрытии<br /> * 0 - (default) не отключать<br /> * 1 - отключать (посылаем CoA на отключение всех сервисов перед тем как закрыть соединение по sa.radius.connection.close.mode)<br /> */<br /> public class ISGServiceActivator<br /> extends AbstractRadiusServiceActivator<br /> implements ServiceActivator<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGServiceActivator.class );<br /> <br /> /**<br /> * per-realm:<br /> * код опции -&gt; набор сервисов ISG<br /> */<br /> protected Map&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt; optionISGServiceMap = new HashMap&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt;();<br /> <br /> /**<br /> * имя(имена) сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND_PACKET = 0;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND = 1;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> */<br /> protected static final int COA_MODE_SUBSCR_COMMAND = 2;<br /> <br /> /**<br /> * Режим отправки команд<br /> */<br /> protected int coaMode;<br /> <br /> @Deprecated<br /> protected static final int CLOSE_MODE_POD_DEPRECATED = 0;<br /> <br /> protected static final int CLOSE_MODE_NONE = 1;<br /> protected static final int CLOSE_MODE_POD = 2;<br /> protected static final int CLOSE_MODE_SUBSCR_COMMAND = 3;<br /> <br /> protected int closeMode;<br /> protected boolean disableServicesOnClose;<br /> <br /> public ISGServiceActivator()<br /> {<br /> super( null, false, &quot;Acct-Session-Id&quot;, false );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object init( Setup setup, int moduleId, InetDevice device, InetDeviceType deviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, device, deviceType, deviceConfig );<br /> <br /> this.coaMode = deviceConfig.getInt( &quot;sa.radius.connection.coa.mode&quot;, deviceConfig.getInt( &quot;radius.coa.mode&quot;, deviceConfig.getInt( &quot;coa.mode&quot;, COA_MODE_SSG_COMMAND ) ) );<br /> <br /> //вендор атрибута cisco-SSG-Account-Info (9)<br /> int ciscoSSGAccountInfo_attribute_vendor=9;<br /> //id атрибута cisco-SSG-Account-Info (250)<br /> int ciscoSSGAccountInfo_attribute_id=250;<br /> <br /> Map&lt;Integer, Set&lt;String&gt;&gt; map;<br /> Set&lt;String&gt; set;<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; raList;<br /> InetOptionRuntimeMap inetOptionRuntimeMap = InetOptionRuntimeMap.getInstance(moduleId);<br /> // определение сервисов на каждой из опций<br /> for(Map.Entry&lt;String, Map&lt;Integer, RadiusAttributeSet&gt;&gt; e_realm : this.optionRadiusAttributesMap.getRealmMap().entrySet()){<br /> <br /> map = this.optionISGServiceMap.get(e_realm.getKey());<br /> if(null==map){<br /> map = new HashMap&lt;Integer, Set&lt;String&gt;&gt;();<br /> this.optionISGServiceMap.put(e_realm.getKey(), map);<br /> }<br /> //Перебираем опции в realm-е<br /> for(Map.Entry&lt;Integer, RadiusAttributeSet&gt; e_option : e_realm.getValue().entrySet()){<br /> logger.info(&quot;option = &quot;+inetOptionRuntimeMap.get(e_option.getKey()).title+&quot;(&quot;+e_option.getKey()+&quot;), realm = &quot;+e_realm.getKey()+&quot;, ra = &quot;+e_option.getValue());<br /> set = null;<br /> raList = e_option.getValue().getAttributes(ciscoSSGAccountInfo_attribute_vendor, ciscoSSGAccountInfo_attribute_id);<br /> if(raList!=null){<br /> for(RadiusAttribute&lt;?&gt; attr : raList){<br /> if(null==set){<br /> set = new HashSet&lt;String&gt;();<br /> }<br /> //вырезаем из атрибута cisco-SSG-Account-Info=ASERVICENAME имя сервиса SERVICENAME<br /> set.add(attr.getValue().toString().substring(1));<br /> }<br /> if(set!=null &amp;&amp; set.size()&gt;0){<br /> map.put(e_option.getKey(), set);<br /> }<br /> }<br /> }<br /> }<br /> <br /> // сервис(ы), отправляемый в режиме Reject-To-Accept<br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;sa.radius.service.disable&quot;, deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> <br /> logger.info( &quot;Disable services: &quot; + disableServiceNames );<br /> <br /> this.closeMode = deviceConfig.getInt( &quot;sa.radius.connection.close.mode&quot;, CLOSE_MODE_POD );<br /> this.disableServicesOnClose = deviceConfig.getInt( &quot;sa.radius.connection.close.disableServices&quot;, 0 ) &gt; 0;<br /> <br /> return null;<br /> }<br /> <br /> /**<br /> *<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionModify( ServiceActivatorEvent e )//TODO добавить timeout, чтобы не отправлять слишком быстро. Дожидаться ответов например.<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection modify: oldState: &quot; + e.getOldState() + &quot;; newState: &quot; + e.getNewState() + &quot;; oldOptionSet: &quot; + e.getOldOptions() + &quot;; newOptionSet: &quot; + e.getNewOptions() );<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( e.getNewState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> <br /> if( e.getOldState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> // отключаем disable сервис и включаем активные опции<br /> return sendCommands( connection, disableServiceNames, optionsToServiceNames(e.getRealm(), e.getNewOptions()) );<br /> }<br /> <br /> Collection&lt;Integer&gt; removeOptions = e.getOptionsToRemove();<br /> Collection&lt;Integer&gt; addOptions = e.getOptionsToAdd();<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), removeOptions), optionsToServiceNames(e.getRealm(), addOptions ) );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionClose( ServiceActivatorEvent e )<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection close&quot; );<br /> <br /> Object result;<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( disableServicesOnClose )<br /> {<br /> result = sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> else<br /> {<br /> result = null;<br /> }<br /> <br /> switch( closeMode )<br /> {<br /> default:<br /> case CLOSE_MODE_NONE:<br /> {<br /> break;<br /> }<br /> <br /> case CLOSE_MODE_POD_DEPRECATED:<br /> case CLOSE_MODE_POD:<br /> {<br /> RadiusPacket request = radiusClient.createDisconnectRequest();<br /> prepareRequest( request, connection );<br /> <br /> logger.info( &quot;Send PoD: \n&quot; + request );<br /> result = radiusClient.sendAsync( request );<br /> <br /> break;<br /> }<br /> <br /> case CLOSE_MODE_SUBSCR_COMMAND:<br /> {<br /> logger.info( &quot;Connection close (logoff)&quot; );<br /> <br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=account-logoff&quot; ) );<br /> <br /> logger.info( &quot;Send logoff CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected Collection&lt;String&gt; optionsToServiceNames(String realm, final Collection&lt;Integer&gt; options)//, final Collection&lt;String&gt; serviceNames )<br /> {<br /> if( options == null || options.size() == 0 )<br /> {<br /> return null;<br /> }<br /> <br /> if(null==realm || &quot;&quot;.equals(realm)){<br /> realm = &quot;default&quot;;<br /> }<br /> <br /> final Set&lt;String&gt; result = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;( options.size() + 2 ) );<br /> <br /> for( Integer option : options )<br /> {<br /> Set&lt;String&gt; serviceNames = this.optionISGServiceMap.get(realm).get(option);<br /> if( serviceNames == null ){<br /> serviceNames = this.optionISGServiceMap.get(&quot;default&quot;).get(option);<br /> }<br /> if( serviceNames != null )<br /> {<br /> result.addAll( serviceNames );<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> /**<br /> * Отправка команд на деактивацию и активацию сервисов<br /> * @param connection - InetConnection<br /> * @param serviceNamesDeactivate - список сервисов, которые нужно деактивировать<br /> * @param serviceNamesActivate - список сервисов, которые нужно активировать<br /> * @return<br /> * @throws Exception<br /> */<br /> protected Object sendCommands( final InetConnection connection, final Collection&lt;String&gt; serviceNamesDeactivate, final Collection&lt;String&gt; serviceNamesActivate )<br /> throws Exception<br /> {<br /> Object result = null;<br /> <br /> if(logger.isInfoEnabled()){<br /> logger.info(&quot;Sending commands to deactivate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesDeactivate)+&quot;]&quot;);<br /> logger.info(&quot;Sending commands to activate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesActivate)+&quot;]&quot;);<br /> }<br /> <br /> switch( coaMode )<br /> {<br /> case COA_MODE_SSG_COMMAND_PACKET:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send deactivate services CoA:\n&quot; + packet );<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send activate services CoA:\n&quot; + packet );<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SSG_COMMAND:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SUBSCR_COMMAND:<br /> default:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=deactivate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=activate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> ==== ProtocolHandler ====<br /> Используется собственный ProtocolHandler, необходимый для поиска сервисов Inet по порту на основе Nas-Port-Id.<br /> Задача в том, чтобы по данным из радиус-пакета авторизации найти и авторизовать сервис в биллинге.<br /> В пакете приходят:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> User-Name=nas-port:XXX.XXX.XXX.XXX:0/0/0/1112<br /> NAS-Port-Id=0/0/0/1112<br /> &lt;/source&gt;<br /> где <br /> XXX.XXX.XXX.XXX - ip-адрес NAS-а<br /> 0/0/0/1112 - слот/карта/интерфейс/инкапсуляция<br /> Было решено использовать NAS-Port-Id<br /> <br /> Схема следующая:<br /> *При подключении клиента конфигурируем имя интерфейса в соответствии с инкапсуляцией: для 'encapsulation dot1Q XXX' интерфейс будет 'interface GigabitEthernet0/0.XXX'<br /> *Заводим интерфейс на устройстве в биллинге: имя=Gi0/0.XXX. ''При необходимости проставляем нужный ifIndex в поле 'интерфейс', если хотим собирать netflow''<br /> *При загрузке (или перечитывании конфигурации) наш ISGIPoEProtocolHandler парсит параметры конфигурации radius.ipoe.nas_port_id.pattern.*, по которым создаёт в памяти соответствие 'NAS-Port-Id'-&gt;'ifaceId' для каждого заведённого в биллинге интерфейса устройства. Например, '0/0/0/1112'-&gt;'id интерфейса с именем Gi0/0.1112 в биллинге'<br /> *При авторизации ISGIPoEProtocolHandler ищет интерфейс по NAS-Port-Id из пакета и проставляет опцию INTERFACE_ID, по которой стандартный радиус-процессор будет затем искать сервис Inet.<br /> *При изменении интерфейсов устройства в биллинге кэш 'NAS-Port-Id'-&gt;'ifaceId' в ISGIPoEProtocolHandler-е автоматически обновляется<br /> <br /> ''Примечание:<br /> Если используется QinQ, то NAS-Port-Id будет вида 0/0/0/105.2015 для: 'encapsulation dot1Q 2015 second-dot1q 105', а имя интерфейса: 'Gi0/0.20150105'. Последнее настраивается в radius.ipoe.nas_port_id.pattern.*''<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.managed.ServerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.bitel.oss.systems.inventory.resource.common.DeviceInterfaceService;<br /> import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterface;<br /> import ru.bitel.oss.systems.inventory.resource.common.event.DeviceInterfaceModifiedEvent;<br /> <br /> import java.util.HashMap;<br /> import java.util.List;<br /> import java.util.Map;<br /> import java.util.SortedMap;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * ProtocolHandler для работы с cisco ip subscriber interface + cisco ISG<br /> * В предобработке устанавливается опция пакета InetRadiusProcessor.INTERFACE_ID, где указывается номер интерфейса в биллинге,<br /> * соответствующий атрибуту Nas-Port-Id из пакета<br /> * Соответствие определяется по шаблонам, заданным в конфигурации, и имени интерфейса сервиса в биллинге<br /> * Предполагается, что клиент авторизуется именно на том устройстве, на котором указан этот ProtocolHandler (не на дочерних)<br /> *<br /> * Параметры конфигурации:<br /> * radius.ipoe.nas_port_id.pattern.[i].pattern - регэксп-шаблон для имени интерфейса в биллинге<br /> * radius.ipoe.nas_port_id.pattern.[i].replacement - выражение для построения nas_port_id по шаблону<br /> *<br /> * Пример:<br /> * Gi0/0.1112 -&gt; 0/0/0/1112<br /> * Gi0/0.20150105 -&gt; 0/0/0/105.2015<br /> *<br /> */<br /> public class ISGIPoEProtocolHandler extends ISGProtocolHandler implements RadiusProtocolHandler {<br /> <br /> private static final Logger logger = Logger.getLogger( ISGIPoEProtocolHandler.class );<br /> <br /> /**<br /> * Кэш интерфейсов устройства<br /> */<br /> private volatile DeviceNasPortMap ifaceMap;<br /> <br /> @Override<br /> public void init(Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig) throws Exception {<br /> super.init(setup, moduleId, inetDevice, inetDeviceType, deviceConfig);<br /> this.ifaceMap = new DeviceNasPortMap(moduleId, inetDevice.getId(), deviceConfig.subIndexed(&quot;radius.ipoe.nas_port_id.pattern.&quot;));<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public void preprocessAccessRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccessRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета InetRadiusProcessor.INTERFACE_ID<br /> * @param request радиус-пакет<br /> */<br /> private void setBGIfaceId(RadiusPacket request) {<br /> String nas_port_id = request.getStringAttribute(-1, RadiusDictionary.NAS_Port_Id, null);<br /> Integer port=-1;<br /> if(nas_port_id!=null){<br /> port = this.ifaceMap.getIfacePort(nas_port_id);<br /> }<br /> if(null==port)<br /> {<br /> port=-1;//Насчёт port=0 и port=-1 - см http://forum.bgbilling.ru/viewtopic.php?f=44&amp;t=7694&amp;p=64541#p64541<br /> }<br /> request.setOption(InetRadiusProcessor.INTERFACE_ID, port);<br /> }<br /> <br /> /**<br /> * Кэш соответствий Nas-Port-Id -&gt; id интерфейса в биллинге для устройства<br /> * Обновляется при изменении порта или перезагрузке конфигурации<br /> */<br /> private class DeviceNasPortMap implements EventListener&lt;DeviceInterfaceModifiedEvent&gt; {<br /> /**<br /> * Соответствие cisco Nas-Port-Id -&gt; id интерфейса в биллинге<br /> */<br /> private volatile Map&lt;String, Integer&gt; nasPortIdToBGPortIdMap;<br /> private final int moduleId;<br /> private final int deviceId;<br /> /**<br /> * Список шаблонов-регулярных выражений, по которым будем получать Nas-Port-Id по названию интерфейса<br /> * Список паттернов не обновляется, т.к. берётся из конфига.<br /> * При перезагрузке конфига в любом случае ISGIPoEProtocolHandler будет переинициализирован целиком<br /> */<br /> private final SortedMap&lt;Integer, ParameterMap&gt; patternMap;<br /> <br /> public DeviceNasPortMap(int moduleId, int deviceId, SortedMap&lt;Integer, ParameterMap&gt; patternMap) throws BGException {<br /> this.moduleId = moduleId;<br /> this.deviceId = deviceId;<br /> this.patternMap = patternMap;<br /> EventProcessor.getInstance().addListener(this, DeviceInterfaceModifiedEvent.class);<br /> this.load();<br /> }<br /> <br /> private synchronized void load(){<br /> this.nasPortIdToBGPortIdMap = new HashMap&lt;String, Integer&gt;();<br /> //Перебираем порты устройств<br /> logger.info(&quot;(Re)loading DeviceNasPortMap for device &quot;+this.deviceId);<br /> ServerContext ctx = (ServerContext) ThreadContext.get();<br /> try {<br /> DeviceInterfaceService devicePortService = ctx.getService(DeviceInterfaceService.class, moduleId);<br /> List&lt;DeviceInterface&gt; deviceIfaceList = devicePortService.devicePortList(this.deviceId);<br /> String nasPortId;<br /> if(deviceIfaceList!=null){<br /> for(DeviceInterface iface : deviceIfaceList){<br /> nasPortId = nasPortIdByIfaceTitle(iface.getTitle());<br /> if(null!=nasPortId){<br /> nasPortIdToBGPortIdMap.put(nasPortId, iface.getPort());<br /> logger.debug(&quot;[device id=&quot; + this.deviceId + &quot;]: nas-port-id='&quot; + nasPortId + &quot;' -&gt; &quot; + iface.getPort());<br /> }<br /> }<br /> }<br /> } catch (BGException e) {<br /> logger.error(&quot;Error (re)loading DeviceNasPortMap&quot;, e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает Nas-Port-Id по имени интерфейса на основе регекспов из patternMap<br /> * @param ifaceTitle имя инерфейса (ex Gi0/0.123)<br /> * @return Nas-Port-Id (ex 0/0/0/123)<br /> */<br /> protected String nasPortIdByIfaceTitle(String ifaceTitle){<br /> if(null==ifaceTitle){<br /> return null;<br /> }<br /> Pattern p;<br /> Matcher m;<br /> String pattern;<br /> String replacement;<br /> String nasPortId;<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; patternMapEntry : patternMap.entrySet()){<br /> pattern = patternMapEntry.getValue().get(&quot;pattern&quot;, null);<br /> replacement = patternMapEntry.getValue().get(&quot;replacement&quot;, null);<br /> if(pattern!=null &amp;&amp; replacement!=null){<br /> p = Pattern.compile(pattern);<br /> m = p.matcher(ifaceTitle);<br /> if (m.find()) {<br /> //Получаем логин путём подстановки найденных capturing groups в $1, $2 и т.д. шаблона<br /> nasPortId = m.replaceFirst(replacement);<br /> return nasPortId;<br /> }<br /> }<br /> }<br /> return null;<br /> }<br /> <br /> /**<br /> * Получаем id порта в биллинге по nas_port_id из кэша<br /> */<br /> public Integer getIfacePort(String nas_port_id) {<br /> return this.nasPortIdToBGPortIdMap.get(nas_port_id);<br /> }<br /> <br /> /**<br /> * Обновляем кэш при изменении интерфейса<br /> * @throws BGException<br /> */<br /> @Override<br /> public void notify(DeviceInterfaceModifiedEvent event, EventListenerContext eventListenerContext) throws BGException {<br /> DeviceInterface deviceIface = event.getNewItem();<br /> if(deviceIface==null){<br /> deviceIface = event.getOldItem();<br /> }<br /> if(deviceIface!=null){<br /> if(deviceIface.getDeviceId()==this.deviceId){<br /> this.load();<br /> }<br /> }<br /> }<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.dhcp.DhcpProtocolHandler;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> <br /> import java.util.Collections;<br /> import java.util.LinkedHashMap;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * Базовый класс для Cisco ISG<br /> * Копипаста бителовского, без лишней потехи с option 82<br /> */<br /> public class ISGProtocolHandler<br /> extends AbstractRadiusProtocolHandler<br /> implements RadiusProtocolHandler, DhcpProtocolHandler<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGProtocolHandler.class );<br /> <br /> /**<br /> * Код атрибута - id родительского аккаунтинга<br /> */<br /> protected int parentAcctSessionIdType;<br /> <br /> /**<br /> * Префикс id родительского аккаунтинга<br /> */<br /> protected String parentAcctSessionIdPrefix;<br /> <br /> /**<br /> * Код атрибута - имя сервиса (для cisco-avpair)<br /> */<br /> protected int serviceNameType;<br /> <br /> /**<br /> * Префикс имени сервиса (для cisco-avpair)<br /> */<br /> protected String serviceNamePrefix;<br /> <br /> /**<br /> * Имя сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> public ISGProtocolHandler()<br /> {<br /> super( 9 ); // Cisco<br /> }<br /> <br /> @Override<br /> public void init( Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, inetDevice, inetDeviceType, deviceConfig );<br /> <br /> parentAcctSessionIdType = deviceConfig.getInt( &quot;radius.parentAcctSessionId.type&quot;, 1 ); // cisco-avpair<br /> parentAcctSessionIdPrefix = deviceConfig.get( &quot;radius.parentAcctSessionId.prefix&quot;, &quot;parent-session-id=&quot; );<br /> serviceNameType = deviceConfig.getInt( &quot;radius.serviceName.type&quot;, 251 ); // cisco-SSG-Service-Info<br /> serviceNamePrefix = deviceConfig.get( &quot;radius.serviceName.prefix&quot;, &quot;&quot; );<br /> <br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )<br /> throws Exception<br /> {<br /> int acctStatusType = request.getIntAttribute( -1, RadiusDictionary.Acct_Status_Type, -1 );<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> <br /> // извлекаем parentAcctSessionId<br /> String parentAcctSessionId;<br /> // если parentAcctSessionId находится в cisco-avpair - то нужно искать по префиксу<br /> if( parentAcctSessionIdType == 1 )<br /> {<br /> parentAcctSessionId = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; attributes = request.getAttributes( radiusVendor, parentAcctSessionIdType );<br /> if( attributes != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; attr : attributes )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> String value = ((RadiusAttribute&lt;String&gt;)attr).getValue();<br /> if( value.startsWith( parentAcctSessionIdPrefix ) )<br /> {<br /> parentAcctSessionId = value.substring( parentAcctSessionIdPrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> parentAcctSessionId = request.getStringAttribute( radiusVendor, parentAcctSessionIdType, null );<br /> }<br /> <br /> // если это аккаунтинг сервисной сессии<br /> if( parentAcctSessionId != null )<br /> {<br /> // извлекаем serviceName<br /> String serviceName;<br /> // если serviceName находится в cisco-avpair - то нужно искать по префиксу<br /> if( serviceNameType == 1 )<br /> {<br /> serviceName = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> final List&lt;RadiusAttribute&lt;?&gt;&gt; ras = request.getAttributes( radiusVendor, serviceNameType );<br /> if( ras != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; ra : ras )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> final String value = ((RadiusAttribute&lt;String&gt;)ra).getValue();<br /> if( value.startsWith( serviceNamePrefix ) )<br /> {<br /> serviceName = value.substring( serviceNamePrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> serviceName = request.getStringAttribute( radiusVendor, serviceNameType, null );<br /> }<br /> <br /> if( serviceName == null || !serviceName.startsWith( &quot;N&quot; ) )<br /> {<br /> logger.error( &quot;Parent acctSessionId found, but ServiceName is not&quot; );<br /> }<br /> else<br /> {<br /> serviceName = serviceName.substring( 1 );<br /> }<br /> <br /> // устанавливаем id родительской сессии<br /> request.setOption( InetRadiusProcessor.PARENT_ACCT_SESSION_ID, parentAcctSessionId );<br /> // устанавливаем имя сервиса текущего аккаунтинга<br /> request.setOption( InetRadiusProcessor.SERVICE_NAME, serviceName );<br /> <br /> // если указан сервис, при котором доступ ограничен - проверяем, не его ли это аккаунтинг,<br /> // и, если это так, переключаем состояние соединения<br /> if( disableServiceNames != null &amp;&amp; disableServiceNames.contains( serviceName ) )<br /> {<br /> // start или update<br /> if( acctStatusType == 1 || acctStatusType == 3 )<br /> {<br /> logger.debug( &quot;State is disable (from start disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_DISABLE );<br /> }<br /> else<br /> {<br /> logger.debug( &quot;State is enable (from stop disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_ENABLE );<br /> }<br /> }<br /> }<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> === Устройства ===<br /> Дерево устройств:<br /> <br /> [[Файл:devices.png]]<br /> <br /> Интерфейсы:<br /> <br /> [[Файл:device-ifaces.png]]<br /> <br /> === IP-ресурсы ===<br /> Не используются.<br /> === VLAN-ресурсы ===<br /> Не используются.<br /> <br /> == Настройка BGInetAccess и BGInetAccounting ==<br /> <br /> === BGInetAccess-VPN ===<br /> <br /> '''access.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3851<br /> MEMORY=-Xmx512m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-access.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;4&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;3&quot;/&gt;<br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3812&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> === BGInetAccess-ISG ===<br /> '''TODO''': описать настройку справочника сервисов ISG.<br /> <br /> У нас для этих целей сейчас используется модуль Dialup - так исторически сложилось.<br /> Но по-хорошему, нужно настроить отдельный BGInetAccess, повешать его на отдельную ProcessGroup и авторизовать сервисы из него со специального служебного договора.<br /> Пример есть в статье [[ISG,_схема_со_стартом_сессии_и_ее_авторизацией_по_IP,_выдача_адресов_на_основе_option82_(Конфигурация_BGBilling%27а)]] (см. 'ASR ISG Service')<br /> <br /> === BGInetAccounting-VPN ===<br /> <br /> '''accounting.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccounting-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-accounting.xml&quot;<br /> NAME=inet-accounting<br /> NAME_SHORT=accounting<br /> ADMIN_PORT=3852<br /> MEMORY=-Xmx1024m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &quot;$@&quot;<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-accounting.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;accounting&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccounting-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;5&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения radius-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Параметры сохранения flow-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.flow.dir&quot; value=&quot;data/flow&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл и поток слушателя --&gt;<br /> &lt;param name=&quot;datalog.flow.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.flow.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Создание Accounting --&gt;<br /> &lt;bean name=&quot;accounting&quot; class=&quot;ru.bitel.bgbilling.modules.inet.accounting.Accounting&quot;/&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3813&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;1 * 1024 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;30&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;500&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.accounting --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.accounting&lt;/param&gt;<br /> &lt;!-- Передача setup --&gt;<br /> &lt;param name=&quot;setup&quot;&gt;setup&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> == Тарифы ==<br /> <br /> Безлимит 1Мбит:<br /> <br /> [[Файл:vpn-unlim.png]]<br /> <br /> = Конфигурация на cisco =<br /> <br /> Пример конфига интерфейса клиента:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> interface GigabitEthernet0/0.127<br /> description ---- TEST VPN ----<br /> encapsulation dot1Q 127<br /> ip vrf forwarding vpn-test<br /> ip address IP.IP.IP.IP MASK.MASK.MASK.MASK<br /> no ip redirects<br /> no ip proxy-arp<br /> ip verify unicast source reachable-via rx<br /> no cdp enable<br /> service-policy type control IP-ISG-VRFSUB<br /> ip subscriber interface<br /> end<br /> &lt;/source&gt;<br /> <br /> ...<br /> <br /> = Дополнительно =<br /> *[[Мониторинг_Inet-Radius_через_JMX]]<br /> * Последние версии кода: [https://github.com/Cromeshnic/BGTools/ на github]<br /> <br /> = TODO =<br /> *Описать настройку [[http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 справочника сервисов ISG]] (Done)<br /> *Выложить дин. код на github<br /> *Описать подключение netflow для детализации<br /> *Скрипт обновления/проставления ifindex на интерфейсах в BG<br /> *Автоконфигурирование интерфейса по telnet/ssh при создании/удалении<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 05:11, 25 июля 2013 (UTC)</div> Mon, 21 Jul 2014 06:35:46 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG Vlan per user + Cisco IP subscriber interface + ISG http://wiki.bitel.ru/index.php/Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG <p>Cromeshnic:&#32;/* TODO */</p> <hr /> <div>= Описание задачи =<br /> Клиентам предоставляется доступ по схеме vlan per user на роутерах cisco.<br /> Необходимо реализовать учёт и управление услугами в модуле Inet.<br /> <br /> = Решение =<br /> Сразу оговоримся, что в нашем случае речь идёт об услуге '''MPLS IP VPN'''.<br /> Для доступа интернет, возможно, появятся какие-то нюансы, но в целом механизм тот же.<br /> <br /> Схема решения такая:<br /> *на клиентском интерфейсе настраивается ip subscriber interface + isg<br /> *интерфейсы авторизуются в BGInetAccess по паре &quot;устройство+интерфейс&quot;<br /> *Настройки скорости выдаются через ISG-сервисы, смена тарифа происходит через CoA<br /> *Трафик собираем по радиус-счётчикам ISG-сервисов и родительской сессии, либо через Netflow<br /> *В сервисе модуля Inet указываем только устройство и интерфейс<br /> <br /> В нашем случае в биллинге не задаются ни сеть IP клиента на интерфейсе, ни его VLAN, т.к. для тарификации они не играют роли.<br /> Но это можно реализовать для учёта ресурсов, а также для автоматического конфигурирования.<br /> <br /> ''Дополнительно усложним задачу:<br /> В некоторых случаях клиенту предоставляется VPN + доступ в интернет через NAT на нашей циске.<br /> Доступ в интернет считается в другом модуле, но такая схема несколько усложняет задачу, поскольку тогда в каждой точке VPN-а нужно разделять трафик на собственно VPN и интернет, чтобы не тарифицировать последний 2 раза. Эту схему мы реализуем отдельным типом сервиса с отдельной привязкой трафика и отдельными сервисами ISG.''<br /> <br /> = Настройка биллинга =<br /> == Настройка модуля ==<br /> Для услуг VPN было решено завести отдельный экземпляр модуля Inet с названием 'VPN'.<br /> <br /> Плюсы:<br /> *Можно учитывать услуги интернета и vpn на одном договоре без проблем с пересечением тарифных планов (основная причина, т.к. в наследство осталось много таких договоров)<br /> *Учёт услуг, визуальное разделение услуг на договоре и в отчётах.<br /> <br /> Минусы:<br /> *Дублирование ресурсов между несколькими экземплярами модулей Inet: одни и те же устройства, vlan, интерфейсы используются в разных местах. Могут быть проблемы с учётом.<br /> <br /> Конфигурация модуля:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Активные и приостановленные статусы договора<br /> contract.status.active.codes=0<br /> contract.status.suspend.codes=3,4<br /> # Проверка цены в тарифе: 0 - проверка отсутсвует, 1 - ошибка, только если у сессии есть трафик определенного типа,<br /> # но для него нет цены, 2 - ошибка, если хотя бы для одного типа трафика в привязке типа сервиса нет цены (по умолчанию - 1)<br /> #http://forum.bgbilling.ru/viewtopic.php?p=65629#p65629<br /> accounting.tariffication.checkPrice=0<br /> # Режим активации учетного периода, если не используется скрипт на событие активации,<br /> # 0 (по умолчанию) - активация со дня подключения (старта сессии), 1 - активация с начала месяца.<br /> # Следует учитывать, что учетный период является второй величиной при вычислении пропорциональности<br /> # в тарифной ветке &quot;Диапазон трафика&quot;<br /> #accounting.period.activation.mode=0<br /> <br /> # Нужно ли отключать сервис с типом инициации &quot;по трафику&quot;, если тариф не найден<br /> #serv.disableOnTariffError=0<br /> <br /> #Пункты Web - меню<br /> #web.menuItem1=Отчет по сессиям Inet<br /> #web.menuItem2=Смена пароля на логины Inet<br /> #web.menuItem3=none<br /> #web.menuItem3=Отчет по трафикам Inet<br /> <br /> # Параметры автоматической генерации логина для сервиса.<br /> # Минимальное значение логина при генерации логина<br /> #serv.login.min=1<br /> # Максимальное значение логина при генерации логина (т.е. если в базе присутствуют логины 1,2,3 и 10000000,<br /> # то при генерации создастся логин 4, а не 10000001)<br /> #serv.login.max=9999999<br /> <br /> # Парамерты автоматической генерации пароля для сервиса. Можно указать в конфигурации модуля, конфигурации устройства, конфигурации типа сервиса<br /> # (в последнем случае значения будут главнее):<br /> # Минимальная длина пароля<br /> serv.password.length.min=5<br /> # Максимальная длина пароля<br /> serv.password.length.max=16<br /> # Разрешенные символы (используются также при генерации пароля)<br /> serv.password.chars=1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz<br /> # Описание разрешенных символов, если пользователь ввел другие<br /> serv.password.chars.description=В пароле допустимы только цифры и латинские буквы.<br /> # Длина для автоматически генерируемого пароля<br /> serv.password.length.auto=6<br /> # Используемые символы для автоматически генерируемого пароля (по умолчанию значение берется из параметра serv.password.chars)<br /> #serv.password.chars.auto=<br /> <br /> # Параметры активации карточек модуля card при использовании InetRadiusProcessor,<br /> # данные параметры можно указать как в конфигурации модуля, так и в конфигурации устройства.<br /> # Код модуля card<br /> #card.moduleId=<br /> # id услуг активации<br /> #card.activate.serviceIds=<br /> # Минимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.min=0<br /> # Максимальное значение карточного логина используется, чтобы указать, какие числовые логины нужно искать в карточках;<br /> # если 0, то ограничение не действует.<br /> #card.login.max=0<br /> &lt;/source&gt;<br /> <br /> Также не забываем настроить задачу планировщика &quot;Активация/деактивация сервисов по периоду&quot; хотя бы раз в сутки в полночь, чтобы корректно обрабатывалось переоформление и перенос сервисов с договора на договор будущим числом.<br /> <br /> == Типы трафиков и привязки ==<br /> === Типы трафиков ===<br /> <br /> [[Файл:traffic_types.png]]<br /> <br /> === Привязки ===<br /> ==== Radius - full ====<br /> Простая привязка с 2 типами трафика: входящий/исходящий, которые берутся из стандартных счетчиков радиус-пакетов родительской сессии.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> &lt;div class=&quot;collapsible collapsed&quot;&gt;[[Файл:radius-full.png]]&lt;/div&gt;<br /> <br /> ==== Radius - NAT ====<br /> Привязка для VPN + NAT.<br /> Для родительской сессии будет 2 дочерних сессии ISG: IPVPN-NAT-INET и IPVPN-NAT-VPN-xxx, где xxx - скорость.<br /> Будем брать трафики из них.<br /> Для входящего: вендор = -2, атрибут = 1<br /> Для исходящего: вендор = -2, атрибут = 2<br /> [[Файл:radius-nat.png]]<br /> <br /> == Типы сервисов ==<br /> <br /> === VPN-IPoE ===<br /> [[Файл:serv-type-ipoe.png]]<br /> <br /> === VPN-IPoE (+NAT) ===<br /> [[Файл:serv-type-ipoe-nat.png]]<br /> <br /> == Опции ==<br /> Заведём опции для соответствующих ISG-сервисов.<br /> Опции FLOWON/FLOWOFF нужны для включения/выключения netflow на интерфейсе<br /> [[Файл:options.png]]<br /> <br /> == Устройства и ресурсы ==<br /> <br /> === Группы устройств ===<br /> Не используются.<br /> === Типы устройств ===<br /> Мы используем 3 типа устройств:<br /> *Группа (ProcessGroup) - пустой тип устройств. Указывается в качестве рута для BGInetAccess и BGInetAccounting (см соответствующий раздел)<br /> *Город - пустой тип устройства, добавлен для разбиения дерева по городам. В будущем, возможно, к нему будут привязываться специфические ProcessHandler-ы, отдельные конфиги или выделяться свои Access и Accounting сервера для каждого города.<br /> *IPoE - тип устройства для цисок с поддержкой ip subscriber interface + ISG<br /> <br /> [[Файл:devicetype-ipoe.png]]<br /> <br /> Конфиг типа устройства IPoE:<br /> &lt;source lang=&quot;ini&quot;&gt;<br /> # Realm default атрибуты<br /> radius.realm.default.attributes=cisco-SSG-Account-Info=ADEFAULT;cisco-avpair=subscriber:accounting-list=BG-DSI-IPVRF<br /> #коды ошибок, которые обрабатываются системой Reject-To-Accept (то же самое, что и realm.reject.error)<br /> #http://bgbilling.ru/v5.2/doc/ch18s20.html<br /> #-------reject-to-accept отсутствует<br /> #radius.disable.accessCodes=4,10,11,12,44<br /> # Какие адреса выдавать при ответе Access-Accept в состоянии disable: <br /> # 0 (по умолчанию) - из radius.disable.ipCategories, 1 - так же, как если бы не было ошибки (в том числе привязанные к сервису в договоре)<br /> #radius.disable.mode=0<br /> # код категории ресурсов Fake пула<br /> #radius.disable.ipCategories=7<br /> # радиус атрибуты, отправляемые в режиме Reject-To-Accept<br /> #radius.disable.attributes=<br /> # Id фиктивного сервиса, к которому будут привязываться сессии, по которым нормальный сервис не был найден (код ошибки: 1, логин не найден).<br /> # Необходим, если в radius.disable.accessCodes присутствует код 1<br /> #radius.disable.servId=<br /> # Атрибуты, при наличии которых соединение должно считаться в состоянии DISABLE (т.е. с ограниченным доступом)<br /> #radius.disable.pattern.attributes=<br /> # Вендор атрибута, где хранится MAC-адрес<br /> # Берём стандартный NAS-Port-Id<br /> radius.macAddress.vendor=-1<br /> # Код атрибута, где хранится MAC-адрес<br /> # Берём NAS-Port-Id<br /> radius.macAddress.type=87<br /> # Префикс атрибута (если есть), где хранится MAC-адрес. Например, для cisco avpair <br /> #radius.macAddress.prefix=<br /> #Порт для отправки PoD и CoA запросов (по умолчанию - порт, заданный в параметрах устройства Хост/порт)<br /> radius.port=1700<br /> #<br /> # Режим поиска сервиса: 0 (по умолчанию) - по логину, 1 - по интерфейсу на устройстве (в предобработке должны быть<br /> # проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или INTERFACE_ID), 2 - по VLAN на устройстве (в предобработке<br /> # должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID), 4 - по VLAN на устройстве или<br /> # дочернем устройстве (в предобработке должны быть проставлены опции AGENT_REMOTE_ID и AGENT_CIRCUIT_ID или VLAN_ID),<br /> # 5 - по MAC-адресу на устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS), 6 - по MAC-адресу на<br /> # устройстве или дочернем устройстве (в предобработке должна быть проставлена опция MAC_ADDRESS).<br /> radius.servSearchMode=1,0<br /> #<br /> # Нужно ли проверять пароль: 0 - нет, 1 (по умолчанию) - да.<br /> radius.password.verification=0<br /> #<br /> # При выдаче access-accept добавлять запись в базу<br /> # необходимо, если используется reject-to-accept и по старт пакету нельзя определить в каком состоянии соединение<br /> #чтобы Access при Access-Accept добавлял соединение в базе со статусом WAIT и указанием выданного состояния и опций<br /> connection.start.fromAccept=1<br /> # Бывают ситуации, когда start-пакет не дошел до Accounting-сервера. В этом случае, при<br /> # 1 (значение по умолчанию) - сессия создастся от текущего момента,<br /> # 2 - Accounting проверит, что время сессии из update/stop пакета не больше, чем значение connection.close.timeout и создаст сессию от ее начала, иначе,<br /> # если время сессии больше чем connection.close.timeout, сессия создастся от текущего момента,<br /> # 0 - сессия без старт-пакета создана не будет.<br /> connection.start.fromUpdate=1<br /> # таймаут перевода соединения в статус suspended при остутствии радиус пакетов<br /> connection.suspend.timeout=1200<br /> # таймаут закрытия соединения при остутствии радиус пакетов (не складывается с connection.suspend.timeout)<br /> connection.close.timeout=1260<br /> #При завершении соединения по сигналу Stop-пакетом (RADIUS-Stop) оно фактически завершается через количество секунд, определяемое переменной connection.finish.timeout. Это позволяет, в частности, реализовать сбор &quot;запоздалой&quot; информации о трафике, которая может прийти после Stop-пакета.<br /> connection.finish.timeout=2<br /> # Проверка на повторную аутентификацию при Access-Request. Бывает нужна в случаях, когда NAS сбрасывает (теряет) сессию, но<br /> # Stop-пакет не присылает и клиент пытается подключиться повторно, но у него стоит ограничение на максимум одну сессию. При совпадении<br /> # callingStationId с одной из активных сессий и установленным параметром: 1 - осуществляется попытка закрытия старой сессии (connectionClose),<br /> # 2 - попытка закрытия сессии (connectionClose) и завершение ее в базе, не дожидаясь стоп пакета, 3 - завершение в базе.<br /> #radius.connection.checkDuplicate=0<br /> #<br /> # Нужно ли убирать домен перед поиском сервиса по логину из поля User-Name. По умолчанию - да (1).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> #для IPoE какой пришёл, такой и берём, там не должно быть лишнего<br /> radius.username.removeDomain=0<br /> #<br /> # Нужно ли убирать пробелы из поля User-Name перед поиском логина. По умолчанию - нет (0).<br /> # Следует отключить, если при посылке CoA и PoD пакетов NAS'у необходим атрибут User-Name.<br /> radius.username.removeWhitespace=0<br /> #<br /> # Шаблон вывода ошибки в мониторе с использованием атрибутов из RADIUS-пакета<br /> #radius.accessError.infoPattern=LOGIN:$User-Name<br /> #<br /> # Параметры активации сервисов<br /> # длина паузы, если возникла ошибка<br /> #sa.error.pause=60<br /> # количество заданий за раз<br /> #sa.batch.size=20<br /> # время (сек) ожидания завершения всех заданий (при асинхронной работе)<br /> #sa.batch.wait=5<br /> # пауза (сек) после обработки заданий<br /> #sa.batch.pause=0<br /> # время (сек) ожидания новой задачи перед вызовом disconnect.<br /> #sa.batch.waitNext=5<br /> #<br /> #----------------------------------------<br /> #параметры обработчика активации сервисов<br /> #----------------------------------------<br /> # откуда при отправке CoA брать атрибуты опций (по умолчанию - те же атрибуты, что выдаются при удачной авторизации)<br /> #sa.radius.option.attributesPrefix=radius.inetOption.<br /> #sa.radius.connection.attributes=NAS-Port, Acct-Session-Id, User-Name, Framed-IP-Address, NAS-IP-Address, NAS-Identifier<br /> sa.radius.connection.attributes=Acct-Session-Id, User-Name<br /> #режим отправки CoA. 0 - команды 0xc и 0xb в одном пакете для всех сервисов, 1 - команды 0xc и 0xb в отдельном пакете для каждого сервиса, 2 - атрибуты subscriber:command= в раздельных пакетах для каждого сервиса<br /> sa.radius.connection.coa.mode=1<br /> #Что делать для закрытия соединения:<br /> # 0 (default) - ничего<br /> # 2 - шлём PoD<br /> # 3 - шлём subscriber:command=account-logoff<br /> sa.radius.connection.close.mode=3<br /> #если dhcp lease time большой, а при положительном балансе доступ нужно дать (даже если адрес сейчас выдан серый), нужно установить 1<br /> sa.radius.connection.coa.onEnable=0<br /> #атрибуты CoA запроса для прекращения доступа (используется при sa.radius.connection.withoutBreak=1)<br /> #sa.radius.disable.attributes={@radius.disable.attributes}<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой CoA<br /> #sa.radius.coa.attributes=<br /> #добавлять ли при отправке CoA атрибуты реалма (для default - из radius.realm.default.attributes)<br /> #sa.radius.realm.addAttributes=0<br /> #фиксированные атрибуты, добавляемые в запрос перед отправкой PoD<br /> #sa.radius.pod.attributes=<br /> #<br /> #<br /> ###### VPN services ######<br /> radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle<br /> #Сопоставление nas-port-id из запроса с id порта в биллинге по имени интерфейса (см ru.dsi.bgbilling.modules.inet.dyn.device.cisco.ISGIPoEProtocolHandler)<br /> radius.ipoe.nas_port_id.pattern.1.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{1,4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.1.replacement=$1/0/$2/$3<br /> radius.ipoe.nas_port_id.pattern.2.pattern=^(?:Gi|Fa)(\d+)/?\d*/(\d+)\.(?=\d{8}$)0{0,3}([1-9]\d{0,3})(?=\d{4}$)0{0,3}([1-9]\d{0,3})$<br /> radius.ipoe.nas_port_id.pattern.2.replacement=$1/0/$2/$4.$3<br /> radius.ipoe.nas_port_id.pattern.3.pattern=^BD(\d+)$<br /> radius.ipoe.nas_port_id.pattern.3.replacement=255/0/$1<br /> &lt;/source&gt;<br /> <br /> Обратите внимание на строчку:<br /> &lt;source lang=&quot;ini&quot;&gt;radius.inetOption.1.template=cisco-SSG-Account-Info=A$optionTitle&lt;/source&gt;<br /> Здесь мы говорим, что все опции модуля, находящиеся в ветке с id=1 (&quot;Группа: VPN&quot;), следует трактовать как сервисы ISG с соответствующим названием.<br /> <br /> Параметры radius.ipoe.nas_port_id.pattern.* нужны для самописного предобработчика радиус-пакетов ISGIPoEProtocolHandler.<br /> В них определяются regexp-шаблоны, по которым сопоставляется значение радиус-атрибута Nas-Port-Id (например, 0/0/0/1112) и название интерфейса в биллинге (Gi0/0.1112).<br /> Подробнее см. описание класса.<br /> <br /> ==== ServciceActivator ====<br /> Для нашей схемы используется модифицированный ISGServciceActivator.<br /> <br /> Отличия от стандартного Бителовского:<br /> - соответствие &quot;опция - сервис ISG&quot; берётся из optionRadiusAttributesMap, чтобы работали новые шаблоны атрибутов (radius.inetOption.1.template)<br /> - в соответствие &quot;опция - сервис ISG&quot; добавлена зависимость от realm-а<br /> - убрано всё, что касается DHCP<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttributeSet;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.access.sa.ServiceActivatorEvent;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetConnection;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusServiceActivator;<br /> import ru.bitel.bgbilling.modules.inet.runtime.InetOptionRuntimeMap;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> <br /> import java.util.*;<br /> <br /> /**<br /> * Конфигурация устройства:<br /> * sa.radius.connection.coa.mode = 1<br /> * режим отправки CoA:<br /> * 0 - отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> * 1 - (default) отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> * 2 - отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> *<br /> * sa.radius.service.disable =<br /> * имена сервисов, при котором доступ отключен<br /> * отправляются в режиме Reject-To-Accept<br /> * по-умолчанию не указано<br /> *<br /> * sa.radius.connection.close.mode = 2<br /> * что делать для закрытия соединения:<br /> * 1 - ничего не делать<br /> * 2 - (default) посылаем PoD<br /> * 3 - посылаем subscriber:command=account-logoff<br /> *<br /> * sa.radius.connection.close.disableServices = 0<br /> * отключать ли сервисы ISG при закрытии<br /> * 0 - (default) не отключать<br /> * 1 - отключать (посылаем CoA на отключение всех сервисов перед тем как закрыть соединение по sa.radius.connection.close.mode)<br /> */<br /> public class ISGServiceActivator<br /> extends AbstractRadiusServiceActivator<br /> implements ServiceActivator<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGServiceActivator.class );<br /> <br /> /**<br /> * per-realm:<br /> * код опции -&gt; набор сервисов ISG<br /> */<br /> protected Map&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt; optionISGServiceMap = new HashMap&lt;String,Map&lt;Integer, Set&lt;String&gt;&gt;&gt;();<br /> <br /> /**<br /> * имя(имена) сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code в одном пакете<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND_PACKET = 0;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-SSG-Command-Code по отдельному пакету на сервис<br /> */<br /> protected static final int COA_MODE_SSG_COMMAND = 1;<br /> <br /> /**<br /> * Отправка в атрибуте cisco-avpair=&quot;subscriber:command=deactivate-service&quot;<br /> */<br /> protected static final int COA_MODE_SUBSCR_COMMAND = 2;<br /> <br /> /**<br /> * Режим отправки команд<br /> */<br /> protected int coaMode;<br /> <br /> @Deprecated<br /> protected static final int CLOSE_MODE_POD_DEPRECATED = 0;<br /> <br /> protected static final int CLOSE_MODE_NONE = 1;<br /> protected static final int CLOSE_MODE_POD = 2;<br /> protected static final int CLOSE_MODE_SUBSCR_COMMAND = 3;<br /> <br /> protected int closeMode;<br /> protected boolean disableServicesOnClose;<br /> <br /> public ISGServiceActivator()<br /> {<br /> super( null, false, &quot;Acct-Session-Id&quot;, false );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object init( Setup setup, int moduleId, InetDevice device, InetDeviceType deviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, device, deviceType, deviceConfig );<br /> <br /> this.coaMode = deviceConfig.getInt( &quot;sa.radius.connection.coa.mode&quot;, deviceConfig.getInt( &quot;radius.coa.mode&quot;, deviceConfig.getInt( &quot;coa.mode&quot;, COA_MODE_SSG_COMMAND ) ) );<br /> <br /> //вендор атрибута cisco-SSG-Account-Info (9)<br /> int ciscoSSGAccountInfo_attribute_vendor=9;<br /> //id атрибута cisco-SSG-Account-Info (250)<br /> int ciscoSSGAccountInfo_attribute_id=250;<br /> <br /> Map&lt;Integer, Set&lt;String&gt;&gt; map;<br /> Set&lt;String&gt; set;<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; raList;<br /> InetOptionRuntimeMap inetOptionRuntimeMap = InetOptionRuntimeMap.getInstance(moduleId);<br /> // определение сервисов на каждой из опций<br /> for(Map.Entry&lt;String, Map&lt;Integer, RadiusAttributeSet&gt;&gt; e_realm : this.optionRadiusAttributesMap.getRealmMap().entrySet()){<br /> <br /> map = this.optionISGServiceMap.get(e_realm.getKey());<br /> if(null==map){<br /> map = new HashMap&lt;Integer, Set&lt;String&gt;&gt;();<br /> this.optionISGServiceMap.put(e_realm.getKey(), map);<br /> }<br /> //Перебираем опции в realm-е<br /> for(Map.Entry&lt;Integer, RadiusAttributeSet&gt; e_option : e_realm.getValue().entrySet()){<br /> logger.info(&quot;option = &quot;+inetOptionRuntimeMap.get(e_option.getKey()).title+&quot;(&quot;+e_option.getKey()+&quot;), realm = &quot;+e_realm.getKey()+&quot;, ra = &quot;+e_option.getValue());<br /> set = null;<br /> raList = e_option.getValue().getAttributes(ciscoSSGAccountInfo_attribute_vendor, ciscoSSGAccountInfo_attribute_id);<br /> if(raList!=null){<br /> for(RadiusAttribute&lt;?&gt; attr : raList){<br /> if(null==set){<br /> set = new HashSet&lt;String&gt;();<br /> }<br /> //вырезаем из атрибута cisco-SSG-Account-Info=ASERVICENAME имя сервиса SERVICENAME<br /> set.add(attr.getValue().toString().substring(1));<br /> }<br /> if(set!=null &amp;&amp; set.size()&gt;0){<br /> map.put(e_option.getKey(), set);<br /> }<br /> }<br /> }<br /> }<br /> <br /> // сервис(ы), отправляемый в режиме Reject-To-Accept<br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;sa.radius.service.disable&quot;, deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> <br /> logger.info( &quot;Disable services: &quot; + disableServiceNames );<br /> <br /> this.closeMode = deviceConfig.getInt( &quot;sa.radius.connection.close.mode&quot;, CLOSE_MODE_POD );<br /> this.disableServicesOnClose = deviceConfig.getInt( &quot;sa.radius.connection.close.disableServices&quot;, 0 ) &gt; 0;<br /> <br /> return null;<br /> }<br /> <br /> /**<br /> *<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionModify( ServiceActivatorEvent e )//TODO добавить timeout, чтобы не отправлять слишком быстро. Дожидаться ответов например.<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection modify: oldState: &quot; + e.getOldState() + &quot;; newState: &quot; + e.getNewState() + &quot;; oldOptionSet: &quot; + e.getOldOptions() + &quot;; newOptionSet: &quot; + e.getNewOptions() );<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( e.getNewState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> <br /> if( e.getOldState() == InetServ.STATE_DISABLE )<br /> {<br /> if( !withoutBreak )<br /> {<br /> return connectionClose( e );<br /> }<br /> <br /> // устанавливаем флаг, что нужно будет поменять состояние соединения в базе<br /> if( needConnectionStateModify )<br /> {<br /> e.setConnectionStateModified( true );<br /> }<br /> <br /> // отключаем disable сервис и включаем активные опции<br /> return sendCommands( connection, disableServiceNames, optionsToServiceNames(e.getRealm(), e.getNewOptions()) );<br /> }<br /> <br /> Collection&lt;Integer&gt; removeOptions = e.getOptionsToRemove();<br /> Collection&lt;Integer&gt; addOptions = e.getOptionsToAdd();<br /> <br /> return sendCommands( connection, optionsToServiceNames(e.getRealm(), removeOptions), optionsToServiceNames(e.getRealm(), addOptions ) );<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public Object connectionClose( ServiceActivatorEvent e )<br /> throws Exception<br /> {<br /> logger.info( &quot;Connection close&quot; );<br /> <br /> Object result;<br /> <br /> final InetConnection connection = e.getConnection();<br /> <br /> if( disableServicesOnClose )<br /> {<br /> result = sendCommands( connection, optionsToServiceNames(e.getRealm(), e.getOldOptions()), disableServiceNames );<br /> }<br /> else<br /> {<br /> result = null;<br /> }<br /> <br /> switch( closeMode )<br /> {<br /> default:<br /> case CLOSE_MODE_NONE:<br /> {<br /> break;<br /> }<br /> <br /> case CLOSE_MODE_POD_DEPRECATED:<br /> case CLOSE_MODE_POD:<br /> {<br /> RadiusPacket request = radiusClient.createDisconnectRequest();<br /> prepareRequest( request, connection );<br /> <br /> logger.info( &quot;Send PoD: \n&quot; + request );<br /> result = radiusClient.sendAsync( request );<br /> <br /> break;<br /> }<br /> <br /> case CLOSE_MODE_SUBSCR_COMMAND:<br /> {<br /> logger.info( &quot;Connection close (logoff)&quot; );<br /> <br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=account-logoff&quot; ) );<br /> <br /> logger.info( &quot;Send logoff CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> protected Collection&lt;String&gt; optionsToServiceNames(String realm, final Collection&lt;Integer&gt; options)//, final Collection&lt;String&gt; serviceNames )<br /> {<br /> if( options == null || options.size() == 0 )<br /> {<br /> return null;<br /> }<br /> <br /> if(null==realm || &quot;&quot;.equals(realm)){<br /> realm = &quot;default&quot;;<br /> }<br /> <br /> final Set&lt;String&gt; result = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;( options.size() + 2 ) );<br /> <br /> for( Integer option : options )<br /> {<br /> Set&lt;String&gt; serviceNames = this.optionISGServiceMap.get(realm).get(option);<br /> if( serviceNames == null ){<br /> serviceNames = this.optionISGServiceMap.get(&quot;default&quot;).get(option);<br /> }<br /> if( serviceNames != null )<br /> {<br /> result.addAll( serviceNames );<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> <br /> /**<br /> * Отправка команд на деактивацию и активацию сервисов<br /> * @param connection - InetConnection<br /> * @param serviceNamesDeactivate - список сервисов, которые нужно деактивировать<br /> * @param serviceNamesActivate - список сервисов, которые нужно активировать<br /> * @return<br /> * @throws Exception<br /> */<br /> protected Object sendCommands( final InetConnection connection, final Collection&lt;String&gt; serviceNamesDeactivate, final Collection&lt;String&gt; serviceNamesActivate )<br /> throws Exception<br /> {<br /> Object result = null;<br /> <br /> if(logger.isInfoEnabled()){<br /> logger.info(&quot;Sending commands to deactivate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesDeactivate)+&quot;]&quot;);<br /> logger.info(&quot;Sending commands to activate services (mode=&quot;+this.coaMode+&quot;): [&quot;+Utils.toString(serviceNamesActivate)+&quot;]&quot;);<br /> }<br /> <br /> switch( coaMode )<br /> {<br /> case COA_MODE_SSG_COMMAND_PACKET:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send deactivate services CoA:\n&quot; + packet );<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> }<br /> <br /> result = radiusClient.sendAsync( packet );<br /> //logger.info( &quot;Send activate services CoA:\n&quot; + packet );<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SSG_COMMAND:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xc&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> String value = &quot;\\0xb&quot; + serviceName;<br /> // добавление cisco-SSG-Command-Code<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 252, value ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> <br /> case COA_MODE_SUBSCR_COMMAND:<br /> default:<br /> {<br /> if( serviceNamesDeactivate != null &amp;&amp; serviceNamesDeactivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesDeactivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=deactivate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send deactivate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> if( serviceNamesActivate != null &amp;&amp; serviceNamesActivate.size() &gt; 0 )<br /> {<br /> for( String serviceName : serviceNamesActivate )<br /> {<br /> RadiusPacket packet = radiusClient.createModifyRequest();<br /> prepareRequest( packet, connection );<br /> <br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:service-name=&quot; + serviceName ) );<br /> packet.addAttribute( new RadiusAttribute.RadiusAttributeString( 9, 1, &quot;subscriber:command=activate-service&quot; ) );<br /> <br /> //logger.info( &quot;Send activate service CoA:\n&quot; + packet );<br /> result = radiusClient.sendAsync( packet );<br /> }<br /> }<br /> <br /> break;<br /> }<br /> }<br /> <br /> return result;<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> ==== ProtocolHandler ====<br /> Используется собственный ProtocolHandler, необходимый для поиска сервисов Inet по порту на основе Nas-Port-Id.<br /> Задача в том, чтобы по данным из радиус-пакета авторизации найти и авторизовать сервис в биллинге.<br /> В пакете приходят:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> User-Name=nas-port:XXX.XXX.XXX.XXX:0/0/0/1112<br /> NAS-Port-Id=0/0/0/1112<br /> &lt;/source&gt;<br /> где <br /> XXX.XXX.XXX.XXX - ip-адрес NAS-а<br /> 0/0/0/1112 - слот/карта/интерфейс/инкапсуляция<br /> Было решено использовать NAS-Port-Id<br /> <br /> Схема следующая:<br /> *При подключении клиента конфигурируем имя интерфейса в соответствии с инкапсуляцией: для 'encapsulation dot1Q XXX' интерфейс будет 'interface GigabitEthernet0/0.XXX'<br /> *Заводим интерфейс на устройстве в биллинге: имя=Gi0/0.XXX. ''При необходимости проставляем нужный ifIndex в поле 'интерфейс', если хотим собирать netflow''<br /> *При загрузке (или перечитывании конфигурации) наш ISGIPoEProtocolHandler парсит параметры конфигурации radius.ipoe.nas_port_id.pattern.*, по которым создаёт в памяти соответствие 'NAS-Port-Id'-&gt;'ifaceId' для каждого заведённого в биллинге интерфейса устройства. Например, '0/0/0/1112'-&gt;'id интерфейса с именем Gi0/0.1112 в биллинге'<br /> *При авторизации ISGIPoEProtocolHandler ищет интерфейс по NAS-Port-Id из пакета и проставляет опцию INTERFACE_ID, по которой стандартный радиус-процессор будет затем искать сервис Inet.<br /> *При изменении интерфейсов устройства в биллинге кэш 'NAS-Port-Id'-&gt;'ifaceId' в ISGIPoEProtocolHandler-е автоматически обновляется<br /> <br /> ''Примечание:<br /> Если используется QinQ, то NAS-Port-Id будет вида 0/0/0/105.2015 для: 'encapsulation dot1Q 2015 second-dot1q 105', а имя интерфейса: 'Gi0/0.20150105'. Последнее настраивается в radius.ipoe.nas_port_id.pattern.*''<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.common.BGException;<br /> import ru.bitel.bgbilling.kernel.container.managed.ServerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventListener;<br /> import ru.bitel.bgbilling.kernel.event.EventListenerContext;<br /> import ru.bitel.bgbilling.kernel.event.EventProcessor;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> import ru.bitel.common.worker.ThreadContext;<br /> import ru.bitel.oss.systems.inventory.resource.common.DeviceInterfaceService;<br /> import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterface;<br /> import ru.bitel.oss.systems.inventory.resource.common.event.DeviceInterfaceModifiedEvent;<br /> <br /> import java.util.HashMap;<br /> import java.util.List;<br /> import java.util.Map;<br /> import java.util.SortedMap;<br /> import java.util.regex.Matcher;<br /> import java.util.regex.Pattern;<br /> <br /> /**<br /> * @author cromeshnic@gmail.com<br /> * ProtocolHandler для работы с cisco ip subscriber interface + cisco ISG<br /> * В предобработке устанавливается опция пакета InetRadiusProcessor.INTERFACE_ID, где указывается номер интерфейса в биллинге,<br /> * соответствующий атрибуту Nas-Port-Id из пакета<br /> * Соответствие определяется по шаблонам, заданным в конфигурации, и имени интерфейса сервиса в биллинге<br /> * Предполагается, что клиент авторизуется именно на том устройстве, на котором указан этот ProtocolHandler (не на дочерних)<br /> *<br /> * Параметры конфигурации:<br /> * radius.ipoe.nas_port_id.pattern.[i].pattern - регэксп-шаблон для имени интерфейса в биллинге<br /> * radius.ipoe.nas_port_id.pattern.[i].replacement - выражение для построения nas_port_id по шаблону<br /> *<br /> * Пример:<br /> * Gi0/0.1112 -&gt; 0/0/0/1112<br /> * Gi0/0.20150105 -&gt; 0/0/0/105.2015<br /> *<br /> */<br /> public class ISGIPoEProtocolHandler extends ISGProtocolHandler implements RadiusProtocolHandler {<br /> <br /> private static final Logger logger = Logger.getLogger( ISGIPoEProtocolHandler.class );<br /> <br /> /**<br /> * Кэш интерфейсов устройства<br /> */<br /> private volatile DeviceNasPortMap ifaceMap;<br /> <br /> @Override<br /> public void init(Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig) throws Exception {<br /> super.init(setup, moduleId, inetDevice, inetDeviceType, deviceConfig);<br /> this.ifaceMap = new DeviceNasPortMap(moduleId, inetDevice.getId(), deviceConfig.subIndexed(&quot;radius.ipoe.nas_port_id.pattern.&quot;));<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * {@inheritDoc}<br /> */<br /> @Override<br /> public void preprocessAccessRequest(RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet) throws Exception {<br /> super.preprocessAccessRequest(request, response, connectionSet);<br /> //по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета<br /> setBGIfaceId( request);<br /> }<br /> <br /> /**<br /> * по Nas-Port-Id в пакете ищем номер порта на устройстве и указываем его в опции пакета InetRadiusProcessor.INTERFACE_ID<br /> * @param request радиус-пакет<br /> */<br /> private void setBGIfaceId(RadiusPacket request) {<br /> String nas_port_id = request.getStringAttribute(-1, RadiusDictionary.NAS_Port_Id, null);<br /> Integer port=-1;<br /> if(nas_port_id!=null){<br /> port = this.ifaceMap.getIfacePort(nas_port_id);<br /> }<br /> if(null==port)<br /> {<br /> port=-1;//Насчёт port=0 и port=-1 - см http://forum.bgbilling.ru/viewtopic.php?f=44&amp;t=7694&amp;p=64541#p64541<br /> }<br /> request.setOption(InetRadiusProcessor.INTERFACE_ID, port);<br /> }<br /> <br /> /**<br /> * Кэш соответствий Nas-Port-Id -&gt; id интерфейса в биллинге для устройства<br /> * Обновляется при изменении порта или перезагрузке конфигурации<br /> */<br /> private class DeviceNasPortMap implements EventListener&lt;DeviceInterfaceModifiedEvent&gt; {<br /> /**<br /> * Соответствие cisco Nas-Port-Id -&gt; id интерфейса в биллинге<br /> */<br /> private volatile Map&lt;String, Integer&gt; nasPortIdToBGPortIdMap;<br /> private final int moduleId;<br /> private final int deviceId;<br /> /**<br /> * Список шаблонов-регулярных выражений, по которым будем получать Nas-Port-Id по названию интерфейса<br /> * Список паттернов не обновляется, т.к. берётся из конфига.<br /> * При перезагрузке конфига в любом случае ISGIPoEProtocolHandler будет переинициализирован целиком<br /> */<br /> private final SortedMap&lt;Integer, ParameterMap&gt; patternMap;<br /> <br /> public DeviceNasPortMap(int moduleId, int deviceId, SortedMap&lt;Integer, ParameterMap&gt; patternMap) throws BGException {<br /> this.moduleId = moduleId;<br /> this.deviceId = deviceId;<br /> this.patternMap = patternMap;<br /> EventProcessor.getInstance().addListener(this, DeviceInterfaceModifiedEvent.class);<br /> this.load();<br /> }<br /> <br /> private synchronized void load(){<br /> this.nasPortIdToBGPortIdMap = new HashMap&lt;String, Integer&gt;();<br /> //Перебираем порты устройств<br /> logger.info(&quot;(Re)loading DeviceNasPortMap for device &quot;+this.deviceId);<br /> ServerContext ctx = (ServerContext) ThreadContext.get();<br /> try {<br /> DeviceInterfaceService devicePortService = ctx.getService(DeviceInterfaceService.class, moduleId);<br /> List&lt;DeviceInterface&gt; deviceIfaceList = devicePortService.devicePortList(this.deviceId);<br /> String nasPortId;<br /> if(deviceIfaceList!=null){<br /> for(DeviceInterface iface : deviceIfaceList){<br /> nasPortId = nasPortIdByIfaceTitle(iface.getTitle());<br /> if(null!=nasPortId){<br /> nasPortIdToBGPortIdMap.put(nasPortId, iface.getPort());<br /> logger.debug(&quot;[device id=&quot; + this.deviceId + &quot;]: nas-port-id='&quot; + nasPortId + &quot;' -&gt; &quot; + iface.getPort());<br /> }<br /> }<br /> }<br /> } catch (BGException e) {<br /> logger.error(&quot;Error (re)loading DeviceNasPortMap&quot;, e);<br /> }<br /> }<br /> <br /> /**<br /> * Возвращает Nas-Port-Id по имени интерфейса на основе регекспов из patternMap<br /> * @param ifaceTitle имя инерфейса (ex Gi0/0.123)<br /> * @return Nas-Port-Id (ex 0/0/0/123)<br /> */<br /> protected String nasPortIdByIfaceTitle(String ifaceTitle){<br /> if(null==ifaceTitle){<br /> return null;<br /> }<br /> Pattern p;<br /> Matcher m;<br /> String pattern;<br /> String replacement;<br /> String nasPortId;<br /> for(Map.Entry&lt;Integer, ParameterMap&gt; patternMapEntry : patternMap.entrySet()){<br /> pattern = patternMapEntry.getValue().get(&quot;pattern&quot;, null);<br /> replacement = patternMapEntry.getValue().get(&quot;replacement&quot;, null);<br /> if(pattern!=null &amp;&amp; replacement!=null){<br /> p = Pattern.compile(pattern);<br /> m = p.matcher(ifaceTitle);<br /> if (m.find()) {<br /> //Получаем логин путём подстановки найденных capturing groups в $1, $2 и т.д. шаблона<br /> nasPortId = m.replaceFirst(replacement);<br /> return nasPortId;<br /> }<br /> }<br /> }<br /> return null;<br /> }<br /> <br /> /**<br /> * Получаем id порта в биллинге по nas_port_id из кэша<br /> */<br /> public Integer getIfacePort(String nas_port_id) {<br /> return this.nasPortIdToBGPortIdMap.get(nas_port_id);<br /> }<br /> <br /> /**<br /> * Обновляем кэш при изменении интерфейса<br /> * @throws BGException<br /> */<br /> @Override<br /> public void notify(DeviceInterfaceModifiedEvent event, EventListenerContext eventListenerContext) throws BGException {<br /> DeviceInterface deviceIface = event.getNewItem();<br /> if(deviceIface==null){<br /> deviceIface = event.getOldItem();<br /> }<br /> if(deviceIface!=null){<br /> if(deviceIface.getDeviceId()==this.deviceId){<br /> this.load();<br /> }<br /> }<br /> }<br /> }<br /> }<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;java&quot;&gt;<br /> package ru.dsi.bgbilling.modules.inet.dyn.device.cisco;<br /> <br /> import org.apache.log4j.Logger;<br /> import ru.bitel.bgbilling.kernel.network.dhcp.DhcpProtocolHandler;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusAttribute;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusDictionary;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusPacket;<br /> import ru.bitel.bgbilling.kernel.network.radius.RadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDevice;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetDeviceType;<br /> import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ;<br /> import ru.bitel.bgbilling.modules.inet.dyn.device.radius.AbstractRadiusProtocolHandler;<br /> import ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor;<br /> import ru.bitel.bgbilling.server.util.Setup;<br /> import ru.bitel.common.ParameterMap;<br /> import ru.bitel.common.Utils;<br /> import ru.bitel.common.sql.ConnectionSet;<br /> <br /> import java.util.Collections;<br /> import java.util.LinkedHashMap;<br /> import java.util.List;<br /> import java.util.Set;<br /> <br /> /**<br /> * Базовый класс для Cisco ISG<br /> * Копипаста бителовского, без лишней потехи с option 82<br /> */<br /> public class ISGProtocolHandler<br /> extends AbstractRadiusProtocolHandler<br /> implements RadiusProtocolHandler, DhcpProtocolHandler<br /> {<br /> private static final Logger logger = Logger.getLogger( ISGProtocolHandler.class );<br /> <br /> /**<br /> * Код атрибута - id родительского аккаунтинга<br /> */<br /> protected int parentAcctSessionIdType;<br /> <br /> /**<br /> * Префикс id родительского аккаунтинга<br /> */<br /> protected String parentAcctSessionIdPrefix;<br /> <br /> /**<br /> * Код атрибута - имя сервиса (для cisco-avpair)<br /> */<br /> protected int serviceNameType;<br /> <br /> /**<br /> * Префикс имени сервиса (для cisco-avpair)<br /> */<br /> protected String serviceNamePrefix;<br /> <br /> /**<br /> * Имя сервиса, при котором доступ отключен.<br /> */<br /> protected Set&lt;String&gt; disableServiceNames;<br /> <br /> public ISGProtocolHandler()<br /> {<br /> super( 9 ); // Cisco<br /> }<br /> <br /> @Override<br /> public void init( Setup setup, int moduleId, InetDevice inetDevice, InetDeviceType inetDeviceType, ParameterMap deviceConfig )<br /> throws Exception<br /> {<br /> super.init( setup, moduleId, inetDevice, inetDeviceType, deviceConfig );<br /> <br /> parentAcctSessionIdType = deviceConfig.getInt( &quot;radius.parentAcctSessionId.type&quot;, 1 ); // cisco-avpair<br /> parentAcctSessionIdPrefix = deviceConfig.get( &quot;radius.parentAcctSessionId.prefix&quot;, &quot;parent-session-id=&quot; );<br /> serviceNameType = deviceConfig.getInt( &quot;radius.serviceName.type&quot;, 251 ); // cisco-SSG-Service-Info<br /> serviceNamePrefix = deviceConfig.get( &quot;radius.serviceName.prefix&quot;, &quot;&quot; );<br /> <br /> List&lt;String&gt; disableServiceNames = Utils.toList( deviceConfig.get( &quot;radius.serviceName.disable&quot;, &quot;&quot; ) );// INET_FAKE<br /> if( disableServiceNames.size() &gt; 0 )<br /> {<br /> this.disableServiceNames = Collections.newSetFromMap( new LinkedHashMap&lt;String, Boolean&gt;() );<br /> this.disableServiceNames.addAll( disableServiceNames );<br /> }<br /> else<br /> {<br /> this.disableServiceNames = null;<br /> }<br /> }<br /> <br /> @Override<br /> public void preprocessAccountingRequest( RadiusPacket request, RadiusPacket response, ConnectionSet connectionSet )<br /> throws Exception<br /> {<br /> int acctStatusType = request.getIntAttribute( -1, RadiusDictionary.Acct_Status_Type, -1 );<br /> super.preprocessAccountingRequest(request, response, connectionSet);<br /> <br /> // извлекаем parentAcctSessionId<br /> String parentAcctSessionId;<br /> // если parentAcctSessionId находится в cisco-avpair - то нужно искать по префиксу<br /> if( parentAcctSessionIdType == 1 )<br /> {<br /> parentAcctSessionId = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> List&lt;RadiusAttribute&lt;?&gt;&gt; attributes = request.getAttributes( radiusVendor, parentAcctSessionIdType );<br /> if( attributes != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; attr : attributes )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> String value = ((RadiusAttribute&lt;String&gt;)attr).getValue();<br /> if( value.startsWith( parentAcctSessionIdPrefix ) )<br /> {<br /> parentAcctSessionId = value.substring( parentAcctSessionIdPrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> parentAcctSessionId = request.getStringAttribute( radiusVendor, parentAcctSessionIdType, null );<br /> }<br /> <br /> // если это аккаунтинг сервисной сессии<br /> if( parentAcctSessionId != null )<br /> {<br /> // извлекаем serviceName<br /> String serviceName;<br /> // если serviceName находится в cisco-avpair - то нужно искать по префиксу<br /> if( serviceNameType == 1 )<br /> {<br /> serviceName = null;<br /> // смотрим во всех cisco-avpair атрибутах<br /> final List&lt;RadiusAttribute&lt;?&gt;&gt; ras = request.getAttributes( radiusVendor, serviceNameType );<br /> if( ras != null )<br /> {<br /> for( RadiusAttribute&lt;?&gt; ra : ras )<br /> {<br /> @SuppressWarnings(&quot;unchecked&quot;)<br /> final String value = ((RadiusAttribute&lt;String&gt;)ra).getValue();<br /> if( value.startsWith( serviceNamePrefix ) )<br /> {<br /> serviceName = value.substring( serviceNamePrefix.length() );<br /> break;<br /> }<br /> }<br /> }<br /> }<br /> else<br /> {<br /> serviceName = request.getStringAttribute( radiusVendor, serviceNameType, null );<br /> }<br /> <br /> if( serviceName == null || !serviceName.startsWith( &quot;N&quot; ) )<br /> {<br /> logger.error( &quot;Parent acctSessionId found, but ServiceName is not&quot; );<br /> }<br /> else<br /> {<br /> serviceName = serviceName.substring( 1 );<br /> }<br /> <br /> // устанавливаем id родительской сессии<br /> request.setOption( InetRadiusProcessor.PARENT_ACCT_SESSION_ID, parentAcctSessionId );<br /> // устанавливаем имя сервиса текущего аккаунтинга<br /> request.setOption( InetRadiusProcessor.SERVICE_NAME, serviceName );<br /> <br /> // если указан сервис, при котором доступ ограничен - проверяем, не его ли это аккаунтинг,<br /> // и, если это так, переключаем состояние соединения<br /> if( disableServiceNames != null &amp;&amp; disableServiceNames.contains( serviceName ) )<br /> {<br /> // start или update<br /> if( acctStatusType == 1 || acctStatusType == 3 )<br /> {<br /> logger.debug( &quot;State is disable (from start disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_DISABLE );<br /> }<br /> else<br /> {<br /> logger.debug( &quot;State is enable (from stop disable service)&quot; );<br /> request.setOption( InetRadiusProcessor.DEVICE_STATE, InetServ.STATE_ENABLE );<br /> }<br /> }<br /> }<br /> }<br /> <br /> }<br /> &lt;/source&gt;<br /> <br /> === Устройства ===<br /> Дерево устройств:<br /> <br /> [[Файл:devices.png]]<br /> <br /> Интерфейсы:<br /> <br /> [[Файл:device-ifaces.png]]<br /> <br /> === IP-ресурсы ===<br /> Не используются.<br /> === VLAN-ресурсы ===<br /> Не используются.<br /> <br /> == Настройка BGInetAccess и BGInetAccounting ==<br /> <br /> === BGInetAccess-VPN ===<br /> <br /> '''access.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3851<br /> MEMORY=-Xmx512m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-access.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;4&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;3&quot;/&gt;<br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3812&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> === BGInetAccess-ISG ===<br /> '''TODO''': описать настройку справочника сервисов ISG.<br /> <br /> У нас для этих целей сейчас используется модуль Dialup - так исторически сложилось.<br /> Но по-хорошему, нужно настроить отдельный BGInetAccess, повешать его на отдельную ProcessGroup и авторизовать сервисы из него со специального служебного договора.<br /> Пример есть в статье [[ISG,_схема_со_стартом_сессии_и_ее_авторизацией_по_IP,_выдача_адресов_на_основе_option82_(Конфигурация_BGBilling%27а)]] (см. 'ASR ISG Service')<br /> <br /> === BGInetAccounting-VPN ===<br /> <br /> '''accounting.sh''':<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccounting-VPN -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-accounting.xml&quot;<br /> NAME=inet-accounting<br /> NAME_SHORT=accounting<br /> ADMIN_PORT=3852<br /> MEMORY=-Xmx1024m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &quot;$@&quot;<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> <br /> '''inet-accounting.xml''':<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;accounting&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccounting-VPN&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;5&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://localhost:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;30&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения radius-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Параметры сохранения flow-пакетов в файлы логов --&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.flow.dir&quot; value=&quot;data/flow&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл и поток слушателя --&gt;<br /> &lt;param name=&quot;datalog.flow.chunk.size&quot; value=&quot;524288&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.flow.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Создание Accounting --&gt;<br /> &lt;bean name=&quot;accounting&quot; class=&quot;ru.bitel.bgbilling.modules.inet.accounting.Accounting&quot;/&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot;/&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3813&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;1 * 1024 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;30&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;500&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.accounting --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.accounting&lt;/param&gt;<br /> &lt;!-- Передача setup --&gt;<br /> &lt;param name=&quot;setup&quot;&gt;setup&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> &lt;/application&gt;<br /> &lt;/source&gt;<br /> <br /> == Тарифы ==<br /> <br /> Безлимит 1Мбит:<br /> <br /> [[Файл:vpn-unlim.png]]<br /> <br /> = Конфигурация на cisco =<br /> <br /> Пример конфига интерфейса клиента:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> interface GigabitEthernet0/0.127<br /> description ---- TEST VPN ----<br /> encapsulation dot1Q 127<br /> ip vrf forwarding vpn-test<br /> ip address IP.IP.IP.IP MASK.MASK.MASK.MASK<br /> no ip redirects<br /> no ip proxy-arp<br /> ip verify unicast source reachable-via rx<br /> no cdp enable<br /> service-policy type control IP-ISG-VRFSUB<br /> ip subscriber interface<br /> end<br /> &lt;/source&gt;<br /> <br /> ...<br /> <br /> = Дополнительно =<br /> *[[Мониторинг_Inet-Radius_через_JMX]]<br /> * Последние версии кода: [https://github.com/Cromeshnic/BGTools/ на github]<br /> <br /> = TODO =<br /> *Описать настройку [[http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 справочника сервисов ISG]]<br /> *Выложить дин. код на github<br /> *Описать подключение netflow для детализации<br /> *Скрипт обновления/проставления ifindex на интерфейсах в BG<br /> *Автоконфигурирование интерфейса по telnet/ssh при создании/удалении<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 05:11, 25 июля 2013 (UTC)</div> Mon, 21 Jul 2014 06:35:16 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Vlan_per_user_%2B_Cisco_IP_subscriber_interface_%2B_ISG Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET1000<br /> cisco-SSG-Service-Info=QU;1000000;D;1000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, мы объединяем в группу MISC и прописываем индивидуально через radius.inetOption.x.attributes<br /> <br /> === Общий алгоритм добавления новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты<br /> <br /> --[[Участник:Cromeshnic|Cromeshnic]] 06:28, 21 июля 2014 (UTC)</div> Mon, 21 Jul 2014 06:28:42 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Общий HOWTO по добавлению новых сервисов */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET1000<br /> cisco-SSG-Service-Info=QU;1000000;D;1000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, мы объединяем в группу MISC и прописываем индивидуально через radius.inetOption.x.attributes<br /> <br /> === Общий алгоритм добавления новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты</div> Mon, 21 Jul 2014 06:23:50 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Схемы подключения http://wiki.bitel.ru/index.php/%D0%A1%D1%85%D0%B5%D0%BC%D1%8B_%D0%BF%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D1%8F <p>Cromeshnic:&#32;/* RADIUS */</p> <hr /> <div>==RADIUS==<br /> *[[CoA/PoD]] (VPN на примере MPD)<br /> *[[Cisco IP Subnet Sessions]]<br /> *[[Справочник Cisco-ISG сервисов]]<br /> <br /> ==DHCP Option 82==<br /> <br /> *[[DHCP.82 авторизация с выдачей динамических адресов]]<br /> *[[Интеграция с FreeBSD-шлюзом по схеме VLAN-per-user и авторизацией через DHCP option 82]]<br /> *[[Настройка разбора опции DHCP.82]]<br /> <br /> ==Смешанные схемы ==<br /> <br /> *[[ISG, схема со стартом сессии и ее авторизацией по IP, выдача адресов на основе option82]] (DHCP82 + ISG с авторизацией по IP-пакету)<br /> *[[Vlan per user + Cisco IP subscriber interface + ISG]]<br /> *[[RedBack_CLIPS]]<br /> *[[Cisco ISG c авторизацией по порту коммутатора]]<br /> <br /> ==Разные решения ==<br /> *[[Статические адреса + Netflow]]<br /> *[[Управление доступом на Linux-маршрутизаторе]]<br /> *[[Реализация обработчика активации сервисов для коммутаторов DES-3526, DES-3550, DES-3828, DES-3852, DGS-3200-10 и им подобных]]<br /> *[[Обработчик активации сервисов для Mikrotik c изменениями скорости ]]<br /> *[[Обработчик активации сервисов по snmp, зависящий от статуса договора ]]<br /> *[[Обработчик активации сервисов для Cisco(управление доступом по acl) ]]<br /> *[[Обработчик протокола radius, подменяющий атрибуты ]]<br /> *[[Обработчик активации сервисов для Mikrotik(по протоколу mikrotik api) ]]<br /> *[[Обработчик активации сервисов для Manad ]]<br /> *[[Обработчик активации сервисов по ssh ]]<br /> *[[Обработчик активации сервисов по telnet ]]<br /> *[[Описание общих параметров для терминальных(ssh/telnet/manad/mikrotik api) обработчиков активации сервисов ]]<br /> *[[Обработчик управления устройством с синхронизацией интерфейсов и их индексов]]</div> Mon, 21 Jul 2014 06:23:05 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D1%85%D0%B5%D0%BC%D1%8B_%D0%BF%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D1%8F Inet-isg-dictionary http://wiki.bitel.ru/index.php/Inet-isg-dictionary <p>Cromeshnic:&#32;переименовал «Inet-isg-dictionary» в «Справочник Cisco-ISG сервисов»:&amp;#32;ибо ваистену</p> <hr /> <div>#перенаправление [[Справочник Cisco-ISG сервисов]]</div> Mon, 21 Jul 2014 06:22:43 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:Inet-isg-dictionary Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;переименовал «Inet-isg-dictionary» в «Справочник Cisco-ISG сервисов»:&amp;#32;ибо ваистену</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET1000<br /> cisco-SSG-Service-Info=QU;1000000;D;1000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, мы объединяем в группу MISC и прописываем индивидуально через radius.inetOption.x.attributes<br /> <br /> === Общий HOWTO по добавлению новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты</div> Mon, 21 Jul 2014 06:22:43 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Нестандартные сервисы */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET1000<br /> cisco-SSG-Service-Info=QU;1000000;D;1000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, мы объединяем в группу MISC и прописываем индивидуально через radius.inetOption.x.attributes<br /> <br /> === Общий HOWTO по добавлению новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты</div> Mon, 21 Jul 2014 06:19:02 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Выдача атрибутов */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET1000<br /> cisco-SSG-Service-Info=QU;1000000;D;1000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, я объединил в группу MISC и прописываю индивидуально через radius.inetOption.x.attributes:<br /> <br /> === Общий HOWTO по добавлению новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты</div> Mon, 21 Jul 2014 06:18:27 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Выдача атрибутов */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET13000<br /> cisco-SSG-Service-Info=QU;1000000;D;1000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, я объединил в группу MISC и прописываю индивидуально через radius.inetOption.x.attributes:<br /> <br /> === Общий HOWTO по добавлению новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты</div> Mon, 21 Jul 2014 06:18:00 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Нестандартные сервисы */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET13000<br /> cisco-SSG-Service-Info=QU;13000000;D;13000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, я объединил в группу MISC и прописываю индивидуально через radius.inetOption.x.attributes:<br /> <br /> === Общий HOWTO по добавлению новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты</div> Mon, 21 Jul 2014 06:17:36 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Настройка сервисов ISG */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET13000<br /> cisco-SSG-Service-Info=QU;13000000;D;13000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;<br /> <br /> === Нестандартные сервисы ===<br /> Сервисы, которые не бьются по шаблонам, я объединил в группу MISC и прописываю индивидуально через radius.inetOption.x.attributes:<br /> <br /> === Общий HOWTO по добавлению новых сервисов ===<br /> * Добавляем опцию<br /> * Прописываем в опции параметры, если необходимо<br /> * Добавляем radius.inetOption.&lt;x&gt;.template или radius.inetOption.&lt;x&gt;.attributes, если необходимо<br /> * На вкладке «Устройства» жмём «Перечитать конфигурацию на серверах»<br /> * Заводим на служебном договоре сервис в модуле «ISG-справочник» с логином=именем опции, паролем «cisco» и одноимённой опцией на сервисе (в разделе «Опции» сервиса)<br /> * Проверяем, как выдаются атрибуты</div> Mon, 21 Jul 2014 06:17:15 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 Справочник Cisco-ISG сервисов http://wiki.bitel.ru/index.php/%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2 <p>Cromeshnic:&#32;/* Настройка сервисов ISG */</p> <hr /> <div>= Описание задачи =<br /> Схемы с использованием Cisco ISG обычно предполагают использование отдельного радиус-сервера для выдачи параметров сервисов ISG.<br /> Для этого часто используется тот же радиус, но удобнее выносить эту логику отдельно. Тем более, что одновременно может быть реализовано несколько разных схем подключения, использующих ISG.<br /> <br /> В данной статье справочник ISG-сервисов поднимается в виде отдельного экземпляра модуля Inet c одним Access-сервером для выдачи атрибутов и без Accounting-серверов.<br /> <br /> Описанное решение настраивалось на версии 5.2<br /> <br /> = Настройка =<br /> == Установка модуля ==<br /> * [http://bgbilling.ru/v6.1/doc/ch01s11.html Устанавливаем] [http://bgbilling.ru/v6.1/doc/ch17.html модуль Inet], если ещё не установлен.<br /> * В клиенте биллинга: Модули -&gt; Редактор модулей и услуг: добавляем новый экземпляр модуля Inet с названием &quot;ISG-справочник&quot;, запоминаем Id модуля (в нашем случае=31), добавляем услугу &quot;Время&quot;. <br /> [[Файл:inet-isg-module.png]]<br /> * Типы трафика и привязки типов трафиков не добавляем<br /> * Конфигурацию модуля не добавляем (если вам не нужно)<br /> * Добавляем тип устройства ISG - для роутеров с ISG. В конфиге этого типа устройства позже будем прописывать радиус-атрибуты наших сервисов<br /> [[Файл:inet-isg-device-type.png]]<br /> * Добавляем тип устройства Access - ProcessGroup для сервера BGInetAccess, конфиг и обработчики - пустые<br /> [[Файл:inet-isg-device-type-root.png]]<br /> * Получили типы устройств:<br /> [[Файл:inet-isg-device-types.png]]<br /> * Накидываем дерево устройств:<br /> [[Файл:inet-isg-device-tree.png]]<br /> * Заводим тип сервиса:<br /> [[Файл:inet-isg-service-type.png]]<br /> == Установка Access-сервера ==<br /> * Устанавливаем Access-сервер [http://bgbilling.ru/v6.1/doc/ch17s13s01s01.html по мануалу]<br /> * Переименовываем в Access-ISG имя директории: /usr/local/BGInetAccess-ISG<br /> * В access.sh: &quot;-Dapp.name=BGInetAccess-ISG&quot;:<br /> &lt;source lang=&quot;bash&quot;&gt;<br /> #!/bin/sh<br /> <br /> cd ${0%${0##*/}}.<br /> <br /> . ./setenv.sh<br /> <br /> APP_HOME=.<br /> CLASSPATH=$APP_HOME:$APP_HOME/lib/ext/bgcommon-boot.jar<br /> COMMON_PARAMS=&quot;-Dnetworkaddress.cache.ttl=3600 -Djava.net.preferIPv4Stack=true -Dboot.info=1 -Dapp.name=BGInetAccess-ISG -Djava.endorsed.dirs=${BGBILLING_SERVER_DIR}/lib/endorsed:${JAVA_HOME}/lib/endorsed&quot;<br /> LOG_PARAMS=&quot;-Dlog.dir.path=log/ -Dlog4j.configuration=log4j-access.xml&quot;<br /> NAME=inet-access<br /> NAME_SHORT=access<br /> ADMIN_PORT=3951<br /> MEMORY=-Xmx256m<br /> <br /> if [ &quot;$1&quot; = &quot;start&quot; ]; then<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${LOG_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid &amp;<br /> else<br /> if [ &quot;$1&quot; = &quot;debug&quot; ]; then<br /> #starting in debug mode<br /> nohup ${JAVA_HOME}/bin/java ${COMMON_PARAMS} ${MEMORY} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} -enableassertions -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=5589,server=y,suspend=n ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} &gt; ./log/${NAME_SHORT}.out 2&gt;&amp;1 &amp; echo $! &gt; .run/${NAME_SHORT}.pid<br /> else<br /> #execute command<br /> ${JAVA_HOME}/bin/java ${COMMON_PARAMS} -Dadmin.port=$ADMIN_PORT -cp ${CLASSPATH} ru.bitel.common.bootstrap.Boot ru.bitel.bgbilling.kernel.application.server.Application ${NAME} $1 $2 $3 $4 $5 $6<br /> fi<br /> fi<br /> &lt;/source&gt;<br /> * В inet-access.xml указываем:<br /> ** app.name=BGInetAccess-ISG<br /> ** app.id - какой у вас свободен (1, если нет других Access/Accounting-серверов)<br /> ** Параметры подключения к базам<br /> ** moduleId - id нашего экземпляра модуля Inet (у меня = 31)<br /> ** rootDeviceId - id корневого устройства типа Access из дерева устройств (у меня = 1)<br /> ** accounting.deviceTypeIds - типы устройств-NAS-ов (у меня = 2)<br /> ** host/port для access-request-ов<br /> <br /> * Для более актуальной настройки inet-access.xml рекомендую свериться с документацией вашей версии.<br /> * Мой текущий inet-access.xml:<br /> &lt;source lang=&quot;xml&quot;&gt;<br /> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br /> &lt;application context=&quot;access&quot;&gt;<br /> &lt;!-- Уникальное имя приложения --&gt;<br /> &lt;param name=&quot;app.name&quot; value=&quot;BGInetAccess-ISG&quot;/&gt;<br /> &lt;!-- Уникальный числовой id приложения --&gt;<br /> &lt;param name=&quot;app.id&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к БД --&gt;<br /> &lt;param name=&quot;db.driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;<br /> &lt;param name=&quot;db.url&quot; value=&quot;jdbc:mysql://127.0.0.1/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.validationTimeout&quot; value=&quot;10&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к slave БД --&gt;<br /> &lt;param name=&quot;db.slave.1.url&quot; value=&quot;jdbc:mysql://192.168.10.10:3306/bgbilling?useUnicode=true&amp;amp;characterEncoding=Cp1251&amp;amp;allowUrlInLocalInfile=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;jdbcCompliantTruncation=false&amp;amp;elideSetAutoCommits=true&amp;amp;useCursorFetch=true&amp;amp;queryTimeoutKillsConnection=true&amp;amp;connectTimeout=1000&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.pswd&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxIdle&quot; value=&quot;10&quot;/&gt;<br /> &lt;param name=&quot;db.slave.1.maxActive&quot; value=&quot;100&quot;/&gt;<br /> &lt;param name=&quot;slave.alarm.second.behind.master&quot; value=&quot;400&quot;/&gt;<br /> &lt;param name=&quot;slave.disable.second.behind.master&quot; value=&quot;390&quot;/&gt;<br /> <br /> &lt;!-- Параметры подключения к MQ --&gt;<br /> &lt;param name=&quot;mq.url&quot; value=&quot;failover:(tcp://127.0.0.1:61616)&quot;/&gt;<br /> &lt;param name=&quot;mq.user&quot; value=&quot;bill&quot;/&gt;<br /> &lt;param name=&quot;mq.pswd&quot; value=&quot;bgbilling&quot;/&gt;<br /> <br /> &lt;!-- id модуля --&gt;<br /> &lt;param name=&quot;moduleId&quot; value=&quot;31&quot;/&gt;<br /> &lt;!-- id корневого устройства --&gt;<br /> &lt;param name=&quot;rootDeviceId&quot; value=&quot;1&quot;/&gt;<br /> &lt;!-- Типы фейковых устройств, являющихся аккаунтинг серверами --&gt;<br /> &lt;param name=&quot;accounting.deviceTypeIds&quot; value=&quot;2&quot;/&gt;<br /> <br /> &lt;!-- Внутренняя переменная приложения, не изменять --&gt;<br /> &lt;param name=&quot;commonIdentifierName&quot; value=&quot;rootDeviceId&quot;/&gt;<br /> <br /> &lt;!-- Параметры сохранения логов данных --&gt;<br /> &lt;!-- Директория, в которую сохранять radius логи --&gt;<br /> &lt;param name=&quot;datalog.radius.dir&quot; value=&quot;data/radius&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.radius.chunk.size&quot; value=&quot;262144&quot; /&gt;<br /> &lt;!-- Сжимать radius логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.radius.compression.type&quot; value=&quot;1&quot; /&gt;<br /> &lt;!-- Директория, в которую сохранять flow логи --&gt;<br /> &lt;param name=&quot;datalog.dhcp.dir&quot; value=&quot;data/dhcp&quot; /&gt;<br /> &lt;!-- Размер блока данных в файле лога, также размер буфера на лог файл --&gt;<br /> &lt;param name=&quot;datalog.dhcp.chunk.size&quot; value=&quot;131072&quot; /&gt;<br /> &lt;!-- Сжимать flow логи: 0 - не сжимать, 1 - zlib --&gt;<br /> &lt;param name=&quot;datalog.dhcp.compression.type&quot; value=&quot;1&quot; /&gt;<br /> <br /> <br /> &lt;!-- Создание Access --&gt;<br /> &lt;bean name=&quot;access&quot; class=&quot;ru.bitel.bgbilling.modules.inet.access.Access&quot; /&gt;<br /> <br /> &lt;context name=&quot;radius&quot;&gt;<br /> &lt;!-- Cоздание процессора radius-пакетов --&gt;<br /> &lt;bean name=&quot;radiusProcessor&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusProcessor&quot;/&gt;<br /> <br /> &lt;!-- Служебный ScheduledExecutorService, необходимый для dataLogger --&gt;<br /> &lt;scheduledExecutorService name=&quot;hrlydtlggr&quot; corePoolSize=&quot;1&quot; /&gt;<br /> <br /> &lt;!-- Cоздание dataLogger, сохраняющего radius-пакеты на диск (только один экземпляр) --&gt;<br /> &lt;bean name=&quot;radiusDataLogger&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.RadiusHourlyDataLogger&quot;&gt;<br /> &lt;param name=&quot;scheduledExecutor&quot;&gt;hrlydtlggr&lt;/param&gt;<br /> &lt;/bean&gt;<br /> <br /> &lt;!-- Cоздание слушателя radius-пакетов на порту с передачей ему процессора и dataLogger --&gt;<br /> &lt;bean name=&quot;radiusListener&quot; class=&quot;ru.bitel.bgbilling.modules.inet.radius.InetRadiusListener&quot;&gt;<br /> &lt;constructor&gt;<br /> &lt;!-- Хост (интерфейс), на котором будет открыт сокет. Если пусто - на всех --&gt;<br /> &lt;param name=&quot;host&quot; value=&quot;&quot;/&gt;<br /> &lt;!-- Порт, на котором будет открыт сокет --&gt;<br /> &lt;param name=&quot;port&quot; value=&quot;3912&quot;/&gt;<br /> &lt;!-- Размер буфера приема слушателя --&gt;<br /> &lt;param name=&quot;recvBufferSize&quot;&gt;512 * 1024&lt;/param&gt;<br /> &lt;!-- Рекомендуемый SO_RCVBUF сокета --&gt;<br /> &lt;param name=&quot;soRCVBUF&quot;&gt;&lt;/param&gt;<br /> &lt;!-- Количество потоков-обработчиков --&gt;<br /> &lt;param name=&quot;threadCount&quot;&gt;10&lt;/param&gt;<br /> &lt;!-- Максимальное количество пакетов в очереди на обработку --&gt;<br /> &lt;param name=&quot;maxQueueSize&quot;&gt;200&lt;/param&gt;<br /> &lt;!-- Передача процессора --&gt;<br /> &lt;param name=&quot;processor&quot;&gt;radiusProcessor&lt;/param&gt;<br /> &lt;!-- Режим работы, RadiusListener.Mode.authentication --&gt;<br /> &lt;param name=&quot;mode&quot;&gt;RadiusListener.Mode.authentication&lt;/param&gt;<br /> &lt;!-- Передача dataLogger --&gt;<br /> &lt;param name=&quot;dataLogger&quot;&gt;radiusDataLogger&lt;/param&gt;<br /> &lt;/constructor&gt;<br /> &lt;/bean&gt;<br /> &lt;/context&gt;<br /> <br /> &lt;/application&gt;<br /> <br /> &lt;/source&gt;<br /> * Запускаем BGInetAccess-ISG, убеждаемся, что он запустился, смотрим логи на предмет ошибок<br /> <br /> == Настройка сервисов ISG ==<br /> Каждому сервису ISG у нас будет соответствовать пара: сервис модуля &quot;ISG-справочник&quot; на договоре + опция модуля &quot;ISG-справочник&quot;.<br /> Например: заводим опцию модуля INET1000 -&gt; заводим сервис договора с логином &quot;INET1000&quot; -&gt; вешаем на сервис опцию -&gt; перечитываем конфигурацию -&gt; авторизуемся.<br /> <br /> === Опции модуля ===<br /> * Заводим дерево опций:<br /> <br /> [[Файл:inet-isg-options.png]]<br /> <br /> * Опции в дереве группируем по типам, чтобы было удобнее настраивать [http://forum.bitel.ru/viewtopic.php?p=58884#p58884 шаблоны атрибутов] в конфиге типа устройства ISG<br /> <br /> Для примера, добавим 2 ISG-сервиса INET1000 и INET448:<br /> <br /> * Прописываем для группы опций INET (id=7) шаблон атрибутов в конфиге типа устройства &quot;ISG&quot;:&lt;source lang=&quot;text&quot;&gt;radius.inetOption.7.template=cisco-avpair=ip:traffic-class=in access-group name internet priority 90;cisco-avpair=ip:traffic-class=in default drop;cisco-avpair=ip:traffic-class=out access-group name internet priority 90;cisco-avpair=ip:traffic-class=out default drop;cisco-avpair=subscriber:accounting-list=ISG-SERVICE;cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE;Acct-Interim-Interval=$acctInterval;cisco-SSG-Service-Info=I$optionTitle;cisco-SSG-Service-Info=QU;;$speed;;D;;$speed;&lt;/source&gt;<br /> [[Файл:inet-isg-options-inet-template.png]]<br /> * В шаблоне 3 параметра:<br /> ** $optionTitle - имя опции в дереве опций, подставляется по-умолчанию, определять явно не нужно<br /> ** $acctInterval - интервал отсылки аккаунтинга по ISG-сессии в секундах<br /> ** $speed - скорость для сервиса<br /> * Пропишем $acctInterval для всех опций типа INET по-умолчанию:<br /> [[Файл:inet-isg-options-inet.png]]<br /> * Добавляем опцию INET1000 в дерево опций:<br /> [[Файл:inet-isg-option-inet1000.png]]<br /> * Добавляем опцию INET448 с Acct-Interim-Interval=300 (для примера):<br /> [[Файл:inet-isg-option-inet448.png]]<br /> <br /> Т.о. мы в конфиге опции INET448 переопределили $acctInterval=300, который по-умолчанию определён для всех опций INET =900<br /> <br /> === Сервисы модуля ===<br /> * Заводим служебный договор для авторизации сервисов. Необходимо, чтобы у него всегда был неотрицательный баланс/лимит, активный статус и т.п. Не лишним будет ограничить к нему доступ и пометить как &quot;Скрытый&quot;.<br /> * Вешаем на него простейший тариф (персональный или глобальный):<br /> [[Файл:inet-isg-tariff.png]]<br /> * Добавляем модуль &quot;ISG-справочник&quot; и заводим на нём сервисы INET1000 и INET448 с нашим типом сервиса &quot;ISG&quot;:<br /> [[Файл:inet-isg-service-inet1000.png]]<br /> * Не забываем привязать к сервису соответствующую ему опцию!<br /> <br /> === Выдача атрибутов ===<br /> Проверяем, что у нас получилось: Устройства -&gt; Перечитать конфигурацию на серверах<br /> <br /> Авторизуем сервис со стороны циски, смотрим логи:<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 175<br /> Authenticator: {2C F0 1A 41 27 AB F1 05 BF CD 9E 56 B1 49 44 9C}<br /> Attributes:<br /> User-Name=INET1000<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET1000<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET1000] Authenticated as inetServId:150<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=150] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [228]<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:32 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 175<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=900<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET13000<br /> cisco-SSG-Service-Info=QU;13000000;D;13000000<br /> <br /> Process time auth: 20<br /> &lt;/source&gt;<br /> <br /> &lt;source lang=&quot;text&quot;&gt;<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] RadiusListenerWorker - REQUEST:<br /> Packet type: Access-Request<br /> Identifier: 24<br /> Authenticator: {81 97 43 38 1D B6 F2 0F B5 4C BF 2F 4D 84 89 2A}<br /> Attributes:<br /> User-Name=INET448<br /> User-Password=cisco<br /> NAS-IP-Address=10.10.10.10<br /> Service-Type=5<br /> <br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetNas - Search by username=INET448<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - [username=INET448] Authenticated as inetServId:96<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - inetServ[id=96] balance ok: 0.00 [-99999999.99]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - TariffOptionMap: {}<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetApplication - OptionSet: [241]<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - Return code=0<br /> radius 07-01/06:25:39 INFO [rdsLstnr-p-9-t-4] InetRadiusProcessor - RESPONSE_BEFORE_POSTPROCESS:<br /> Packet type: Access-Accept<br /> Identifier: 24<br /> Authenticator: {}<br /> Attributes:<br /> Acct-Interim-Interval=300<br /> cisco-avpair=ip:traffic-class=in access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=in default drop<br /> cisco-avpair=ip:traffic-class=out access-group name internet priority 90<br /> cisco-avpair=ip:traffic-class=out default drop<br /> cisco-avpair=subscriber:accounting-list=ISG-SERVICE<br /> cisco-avpair=subscriber:policy-directive=authenticate aaa list ISG-SERVICE<br /> cisco-SSG-Service-Info=IINET448<br /> cisco-SSG-Service-Info=QU;448500;224250;D;448500;224250<br /> &lt;/source&gt;</div> Mon, 21 Jul 2014 06:09:27 GMT Cromeshnic http://wiki.bitel.ru/index.php/%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA_Cisco-ISG_%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2