Персональные цены для договоров

Материал из BiTel WiKi

(Различия между версиями)
Перейти к: навигация, поиск
(Кэш таблицы переопределений цен в памяти для ускорения тарификации)
Строка 59: Строка 59:
=== Кэш таблицы переопределений цен в памяти для ускорения тарификации ===
=== Кэш таблицы переопределений цен в памяти для ускорения тарификации ===
-
Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object|DAO] для работы с таблицей.
+
Для начала напишем [https://ru.wikipedia.org/wiki/Data_Access_Object DAO] для работы с таблицей.
Объект:
Объект:

Версия 06:09, 8 мая 2017

Внимание! Данное решение/метод/статья относится к версии 5.2 и для других версий может быть неактуальна! Вам нужно самостоятельно поправить решение под свои нужды или воспользоваться помощью на форуме. Будем признательны, если внизу страницы или отдельной статьёй вы разместите исправленное решение для другой версии или подсказки что надо исправить.

Содержание

Введение

Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов. В текущей реализации биллинга это можно сделать только через персональные тарифы. Но персональные тарифы очень неудобны:

  • Ломают отчёты по тарифам
  • Осложняют внесение изменений в глобальные тарифы, от которых они наследованы
  • Требуют специальных знаний и прав доступа для корректного использования

В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов

Описание решения

Тезисно решение выглядит следующим образом

  1. Храним персональные цены на услуги в собственной таблице custom_tariff_cost: contract_tariff_id, sid -> value
  2. Меняем стандартную логику тарифного узла "стоимость" модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены
  3. Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами
  4. Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен

!!! Внимание! пп. 2 и 3 являются "хаками" стандартного поведения биллинга: требуется подмена стандартных классов своими !!!

Скриншоты:

Интерфейс переопределения цен:

Файл:custom_cost.jpg

Отображение на договоре:

Файл:custom_cost1.jpg

В тарифном дереве:

Файл:custom_cost2.jpg

Реализация

Переопределение цены

Таблица для хранения переопределений цен

CREATE TABLE `custom_tariff_cost` (
  `sid` int(10) UNSIGNED NOT NULL,
  `value` decimal(15,5) NOT NULL,
  `contract_tariff_id` int(10) UNSIGNED NOT NULL,
  PRIMARY KEY (`sid`,`contract_tariff_id`),
  KEY `sid` (`sid`)

Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id->contract_tariff.id).

Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.

Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.

Кэш таблицы переопределений цен в памяти для ускорения тарификации

Для начала напишем DAO для работы с таблицей.

Объект:

package ru.dsi.bgbilling.kernel.discount.api.common.bean;
 
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
 
/**
 * @author cromeshnic@gmail.com
 */
public class CustomTariffCost implements Serializable {
    private int contract_tariff_id;
    private int sid;
    private BigDecimal value;
    private Date date1;
    private Date date2;
 
    public CustomTariffCost(){
        this.contract_tariff_id = -1;
        this.sid = -1;
        this.value = null;
        this.date1 = null;
        this.date2 = null;
    }
 
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        sb.append("contract_tariff_id = ")
                .append(this.contract_tariff_id)
                .append(", sid = ")
                .append(this.sid)
                .append(", value = ")
                .append(this.value)
                .append("]");
        return sb.toString();
    }
 
    //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {
    public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {
        //this.cid = cid;
        this.contract_tariff_id = contract_tariff_id;
        this.sid = sid;
        this.value = value;
        this.date1 = date1;
        this.date2 = date2;
    }
 
    public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){
        this.contract_tariff_id = contract_tariff_id;
        this.sid = sid;
        this.value = value;
        this.date1 = null;
        this.date2 = null;
    }
 
    public int getContract_tariff_id() {
        return contract_tariff_id;
    }
 
    public void setContract_tariff_id(int contract_tariff_id) {
        this.contract_tariff_id = contract_tariff_id;
    }
 
    public int getSid() {
        return sid;
    }
 
    public void setSid(int sid) {
        this.sid = sid;
    }
 
    public BigDecimal getValue() {
        return value;
    }
 
    public void setValue(BigDecimal value) {
        this.value = value;
    }
 
    public Date getDate1() {
        return date1;
    }
 
    public void setDate1(Date date1) {
        this.date1 = date1;
    }
 
    public Date getDate2() {
        return date2;
    }
 
    public void setDate2(Date date2) {
        this.date2 = date2;
    }
}

События изменения (для кэша):

package ru.dsi.bgbilling.kernel.discount.bean.event;
 
import ru.bitel.bgbilling.kernel.event.Event;
import ru.bitel.common.SerialUtils;
 
import javax.xml.bind.annotation.XmlRootElement;
 
/**
 * @author cromeshnic@gmail.com
 * Событие при изменении персональной цены на договоре
 */
@XmlRootElement
public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent
{
    private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);
 
    public ContractCustomTariffCostsChangedEvent(int cid, int userId) {
        super(0, cid, userId);
    }
 
    protected ContractCustomTariffCostsChangedEvent(){
    }
 
}
package ru.dsi.bgbilling.kernel.discount.bean.event;
 
import ru.bitel.bgbilling.kernel.event.Event;
import ru.bitel.common.SerialUtils;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
 
import javax.xml.bind.annotation.XmlRootElement;
 
/**
 * @author cromeshnic@gmail.com
 * Событие при изменении персональной цены на тарифе договора
 */
@XmlRootElement
public class CustomTariffCostChangedEvent extends Event//QueueEvent
{
    private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);
    private CustomTariffCost cost;
 
    public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {
        super(0, cid, userId);
        this.cost = cost;
    }
 
    public CustomTariffCost getCost(){
        return this.cost;
    }
 
    protected CustomTariffCostChangedEvent(){
        this.cost = null;
    }
 
}

DAO:

package ru.dsi.bgbilling.kernel.discount.bean;
 
import bitel.billing.common.TimeUtils;
import bitel.billing.server.contract.bean.ContractTariff;
import bitel.billing.server.contract.bean.ContractTariffManager;
import org.apache.log4j.Logger;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.server.util.ServerUtils;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;
import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
 
/**
 * @author cromeshnic@gmail.com
 * Dao для работы с персональными ценами
 */
public class CustomTariffCostDao{
    protected Connection con;
 
    private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);
 
    public CustomTariffCostDao(Connection con) {
        this.con = con;
    }
 
    private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{
        PreparedStatement ps = this.con.prepareStatement("delete from custom_tariff_cost where contract_tariff_id=?");
        ps.setInt(1, contract_tariff_id);
        ps.executeUpdate();
        ps.close();
    }
 
    public void updateContractCustomTariffCosts(ContractTariff ct, List<CustomTariffCost> costs, int userId) throws SQLException, BGException{
        this.removeContractCustomTariffCosts(ct.getId());
        if(null!=costs){
            for(CustomTariffCost cost : costs){
                if(ct.getId()!=cost.getContract_tariff_id()){
                    throw new BGException("Attempt to update custom tariff cost: "+cost.toString()+" for another contract tariff = "+ct.getId());
                }
                this.update0(cost);
            }
        }
 
        EventProcessor.getInstance().publish(new
                ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));
    }
 
    public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {
        ContractTariffManager ctm = new ContractTariffManager(this.con);
        ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());
        cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));
        cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));
        update0(cost);
 
        EventProcessor.getInstance().publish(new
                CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));
        return cost;
    }
 
    private void update0(CustomTariffCost cost) throws SQLException, BGException{
        PreparedStatement ps = this.con.prepareStatement(
                "insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) " +
                        "on duplicate key update value=?");
        ps.setInt(1, cost.getContract_tariff_id());
        ps.setInt(2, cost.getSid());
        ps.setBigDecimal(3, cost.getValue());
        ps.setBigDecimal(4, cost.getValue());
        ps.executeUpdate();
        ps.close();
        ServerUtils.commitConnection(this.con);
    }
 
    public List<CustomTariffCost> getContractTariffCosts(ContractTariff ct) throws BGException {
        List<CustomTariffCost> result = new ArrayList<CustomTariffCost>();
        try{
            PreparedStatement ps = this.con.prepareStatement("select " +
                    "   contract_tariff_id, " +
                    "   sid, " +
                    "   value " +
                    "from " +
                    "   custom_tariff_cost ctc " +
                    "where " +
                    "   ctc.contract_tariff_id=? ");
            ps.setInt(1, ct.getContractId());
            ResultSet rs = ps.executeQuery();
            while(rs.next()){
                result.add(new CustomTariffCost(
                        rs.getInt(1),
                        rs.getInt(2),
                        rs.getBigDecimal(3),
                        TimeUtils.convertCalendarToDate(ct.getDate1()),
                        TimeUtils.convertCalendarToDate(ct.getDate2())));
            }
            rs.close();
            ps.close();
        }catch (SQLException e){
            processException(e);
        }
 
        return result;
    }
 
    public List<CustomTariffCost> getContractTariffCosts(int contract_tariff_id) throws BGException{
        List<CustomTariffCost> result = new ArrayList<CustomTariffCost>();
        try{
            PreparedStatement ps = this.con.prepareStatement("select " +
                    "   ctc.contract_tariff_id, " +
                    "   ctc.sid, " +
                    "   ctc.value," +
                    "   ct.date1," +
                    "   ct.date2 " +
                    "from " +
                    "   custom_tariff_cost ctc left join " +
                    "   contract_tariff ct on ctc.contract_tariff_id=ct.id " +
                    "where " +
                    "   ctc.contract_tariff_id=? ");
            ps.setInt(1, contract_tariff_id);
            ResultSet rs = ps.executeQuery();
            while(rs.next()){
                result.add(new CustomTariffCost(
                        rs.getInt(1),
                        rs.getInt(2),
                        rs.getBigDecimal(3),
                        TimeUtils.convertSqlDateToDate(rs.getDate(4)),
                        TimeUtils.convertSqlDateToDate(rs.getDate(5))));
            }
            rs.close();
            ps.close();
        }catch (SQLException e){
            processException(e);
        }
 
        return result;
    }
 
    protected void processException(SQLException e)
            throws BGException
    {
        throw new BGException(e.getMessage() + " [" + e.getSQLState() + ", " + e.getErrorCode() + "]", e);
    }
}

Сам кэш переопределений цен:

package ru.dsi.bgbilling.kernel.discount.bean;
 
import bitel.billing.common.TimeUtils;
import org.apache.log4j.Logger;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.event.EventListener;
import ru.bitel.bgbilling.kernel.event.EventListenerContext;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;
import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;
 
import java.lang.ref.SoftReference;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
 
/**
 * Кэш переопределённых цен на договоре
 */
public class CustomTariffCostCache {
 
    private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();
    private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);
 
    //cid -> tree_id -> sid -> List<ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost>
    private final ConcurrentMap<Integer, SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>> tariffCostMap =
            new ConcurrentHashMap<Integer, SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>>();
 
    private CustomTariffCostCache() {
        new Thread("custom-tariff-cache-reload")
        {
            public void run()
            {
                try
                {
                    //При изменении переопределённых цен на тарифе обновляем все переопределения для cid
                    EventProcessor.getInstance().addListener(new EventListener<CustomTariffCostChangedEvent>()
                            {
                                @Override
                                public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)
                                        throws BGException
                                {
                                    try {
                                        CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());
                                    } catch (SQLException ex) {
                                        throw new BGException(ex);
                                    }
                                }
 
                            }
                            , CustomTariffCostChangedEvent.class);
 
                    //При изменении переопределённых цен на договоре обновляем все переопределения для cid
                    EventProcessor.getInstance().addListener(new EventListener<ContractCustomTariffCostsChangedEvent>()
                    {
                        @Override
                        public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)
                                throws BGException
                        {
                            try {
                                CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());
                            } catch (SQLException ex) {
                                throw new BGException(ex);
                            }
                        }
 
                    }
                    , ContractCustomTariffCostsChangedEvent.class);
 
 
                    //При изменении тарифа договора обновляем все переопределения для cid
                    EventProcessor.getInstance().addListener(new EventListener<ContractTariffChangedEvent>()
                            {
                                @Override
                                public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)
                                        throws BGException
                                {
                                    try{
                                        CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());
                                    } catch (SQLException ex) {
                                        throw new BGException(ex);
                                    }
                                }
 
                            }
                            , ContractTariffChangedEvent.class);
                }catch (BGException e){
                    CustomTariffCostCache.logger.error(e.getMessage(), e);
                }
            }
        }
        .start();
    }
 
    public static CustomTariffCostCache getInstance(){
        return instance;
    }
 
    /**
     * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.
     * Если таких переопределений несколько - возвращаем первое.
     * (Предполагаем, что переопределение только одно в каждый момент времени)
     * @param cid cid
     * @param sid sid
     * @param tree_id id тарифного дерева
     * @param dt дата, на которую должно быть активно переопределение
     * @return объект переопределения цены
     */
    public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {
 
        SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>> cidCostMapRefRef =
                this.tariffCostMap.get(cid);
        AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> cidCostMapRef = null;
        if(cidCostMapRefRef!=null){
            cidCostMapRef = cidCostMapRefRef.get();
        }
        if(cidCostMapRef==null){//нет - значит нужно получить
            cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);
        }
        if(cidCostMapRef==null || cidCostMapRef.get()==null){
            //В кэше ничего нет для этого cid,
            // но есть пустой cidCostMapRef => переопределений цены на договоре нет
            //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)
            return null;
        }
        ConcurrentMap<Integer, List<CustomTariffCost>> cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);
        if(cidTreeIdCostMap!=null){
            List<CustomTariffCost> costMapList = cidTreeIdCostMap.get(sid);
            if(costMapList!=null){
                //ищем в списке подходящее по датам переопределение
                for(CustomTariffCost cost : costMapList){
                    if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&&
                            (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){
                        return cost;
                    }
                }
            }
 
        }
 
        return null;
    }
 
    private AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {
        //Берём новую мэпу с костами для cid
        ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> cidCustomCosts = loadCidCustomCosts(con, cid);
        SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>> costMapRefRef = this.tariffCostMap.get(cid);
        if(costMapRefRef==null){
            costMapRefRef = new SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>(new AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>(cidCustomCosts));
            this.tariffCostMap.put(cid,costMapRefRef);
        }else{
            AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> costMapRef = costMapRefRef.get();
            if(costMapRef!=null){
                costMapRef.set(cidCustomCosts);
            }else{
                costMapRefRef = new SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>(new AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>(cidCustomCosts));
                this.tariffCostMap.put(cid,costMapRefRef);
            }
        }
        return costMapRefRef.get();
    }
 
    private ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> loadCidCustomCosts(Connection con, int cid) throws SQLException {
        ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> result = null;
        PreparedStatement ps = con.prepareStatement(
            "select " +
                "tp.tree_id, " +
                "ctc.sid, " +
                "ct.id, " +
                "ctc.value, " +
                "ct.date1, " +
                "ct.date2 " +
            "from " +
                "contract_tariff ct left join " +
                "custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join " +
                "tariff_plan tp on ct.tpid=tp.id "+
            "where " +
                "ct.cid=? AND " +
                "not ctc.contract_tariff_id is null AND " +
                "not tp.tree_id is null ");
        ps.setInt(1,cid);
        ResultSet rs = ps.executeQuery();
        int tree_id;
        int sid;
        int contract_tariff_id;
        BigDecimal value;
        Date date1;
        Date date2;
        ConcurrentMap<Integer, List<CustomTariffCost>> integerListConcurrentMap;
        List<CustomTariffCost> customTariffCosts;
        while(rs.next()){
            tree_id = rs.getInt(1);
            sid = rs.getInt(2);
            contract_tariff_id = rs.getInt(3);
            value = rs.getBigDecimal(4);
            date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));
            date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));
 
            if(null==result){
                result = new ConcurrentHashMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>();
            }
            integerListConcurrentMap = result.get(tree_id);
            if(null==integerListConcurrentMap){
                integerListConcurrentMap = new ConcurrentHashMap<Integer, List<CustomTariffCost>>();
                result.put(tree_id, integerListConcurrentMap);
            }
            customTariffCosts = integerListConcurrentMap.get(sid);
            if(null==customTariffCosts){
                customTariffCosts = new ArrayList<CustomTariffCost>();
                integerListConcurrentMap.put(sid, customTariffCosts);
            }
            customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));
        }
        rs.close();
        ps.close();
        return result;
    }
}

Альтернативный узел "Стоимость" модуля NPay

Берём стандартный узел "Стоимость" и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений:

Личные инструменты