Автоопределение города пользователя при оформлении заказа (на основе GeoIP)

#
Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
Я тут в соседнем разделе пообещал примерчик определения города с помощью GeoIP, но подумал что лучше для него создать отдельную тему в "Полезных решениях".

Итак, задача определить город пользователя и подставить его в форму ввода адреса, так чтобы пользователю осталось только вписать улицу-дом-квартиру.

Для начала надо выбрать сервис, который поможет нам определить город. Сервис должен содержать достаточно подробную информацию о России, и обладать возможностью взаимодействовать с клиентскими приложениями через XML-запросы. В качестве такого сервиса я выбрал http://ipgeobase.ru/
Для некоторых возможно будет недостатком то, что этот сервис определяет только города России, не охватывая даже ближнего зарубежья, но я полагаю в сети для него есть немало аналогов.

Сервис выбран. Теперь нам потребуется php-класс, осуществляющий взаимодействие с ним.

Для этого копируем нижеприведенный код, и сохраняем его в папку /modules/ под именем geoip.class.php
// файл /modules/geoip.class.php

<?php
/*
This program is free software. You can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License.

Home:   http://www.it2k.ru/projects/class-ipgeo/
Author: Egor N. Zuskin

Adapted for HostCMS by James V. Kotoff

Simple for php:
$ipList = new IPGeo("xxx.xxx.xxx.xxx,xxx.xxx.xxx.xxx");
print $ipList->ip("xxx.xxx.xxx.xxx"); // city: xxxx
or
$ipList = new IPGeo(array("xxx.xxx.xxx.xxx", "xxx.xxx.xxx.xxx"));
print $ipList->ip("xxx.xxx.xxx.xxx","region"); // region: xxxx
or
$ipList = new IPGeo("xxx.xxx.xxx.xxx");
print $ipList->ip("xxx.xxx.xxx.xxx", "district"); // district: xxxx
*/

DEFINE("IPGEO_SERVER", "194.85.91.253"); // сервер ip geo
DEFINE("IPGEO_SERVER_PORT", 8090); // порт
DEFINE("IPGEO_DEFAULT_PARAM", "city"); // поле возвращаемое поумолчанию
DEFINE("IPGEO_DEBUG", false); // признак отладки (не обращается к серверу)

/**
* @author ice
* Класс для получения ip адресов с сервиса ipgeobase.ru
*/
class IPGeo
{
    var $xml = ""; // текст возвращаемого xml
    var $ip_arr = array(); // массив ip адресов
    var $fields_arr = array("all"); // список запрашиваемых полей    
    var $cache = array(); // кешь ответа

    /**
     * Создание класса и запрос к серверу
     * @param $AIpList список ip адресов, строкой либо строкой через запятую либо массивом
     * @return bool
     */
    function IPGeo($AIpList)
    {

        if (IPGEO_DEBUG) {
            return true;
        }

        if (is_array($AIpList)) {
            $ip_arr = $AIpList;
        } else {
            if (strpos($AIpList, ",") === false) {
                $ip_arr = array(trim($AIpList));
            } else {
                $ip_arr = explode(",", trim($AIpList));
            }
        }

        $ip_arr = array_unique($ip_arr);
        $ip_arr = $this->check_ip_list_valid($ip_arr);

        if (count($ip_arr) == 0)
            return false;

        $ips = "<ip>" . implode("</ip><ip>", $ip_arr) . "</ip>";
        $fields = "<" . implode("/><", $this->fields_arr) . "/>";
        $post_string = "<ipquery><fields>" . $fields . "</fields><ip-list>" . $ips .
            "</ip-list></ipquery>";

        if (!$socket = fsockopen(IPGEO_SERVER, IPGEO_SERVER_PORT))
            return false;

        $query = "POST /geo/geo.html HTTP/1.1\r\n";
        $query .= "Content-Length: " . strlen($post_string) . "\r\n";
        $query .= "\r\n";
        $query .= $post_string;
        $query .= "\r\n\r\n";

        $response = "";
        fwrite($socket, $query);
        while (!feof($socket)) {
            $response .= fgets($socket, 2048);
        }
        fclose($socket);
        $this->xml = trim(substr($response, strpos($response, "\r\n\r\n")));

        return true;
    }

    /**
     * Возвращает запрошенное поле для ip адреса
     * @param $AIp        IP адрес
     * @param $AFieldName Поле
     * @return string
     */
    function ip($AIp, $AFieldName = IPGEO_DEFAULT_PARAM)
    {
        if (IPGEO_DEBUG) {
            return false;
        }

        if (isset($this->cache[$AIp][$AFieldName])) {
            return $this->cache[$AIp][$AFieldName];
        } else {
            if ($this->xml) {
                $doc = new DOMDocument;
                $doc->loadXML($this->xml);
                $xmlpath = new domxpath($doc);
                $ip_ansver = $doc->getElementsByTagName("ip-answer")->item(0);
                $items = $xmlpath->query("ip", $ip_ansver);
                foreach ($items as $it) {
                    $ip = $it->getAttribute('value');
                    if ($ip == $AIp) {
                        $message = @$xmlpath->query("message", $it)->item(0)->nodeValue;
                        $field_value = ($message <> "") ? false : iconv("UTF-8", "CP1251", $xmlpath->
                            query($AFieldName, $it)->item(0)->nodeValue);
                        $this->cache[$AIp][$AFieldName] = $field_value;
                        return $field_value;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Возвращает список правильных ip адресов проверенных по маске xxx.xxx.xxx.xxx < 256
     * @param $AIpList масив ip адресов
     * @return array
     */
    function check_ip_list_valid($AIpList)
    {
        $return = array();

        foreach ($AIpList as $ip) {
            if (ereg("([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3}).([0-9]{1,3})", $ip, $par)) {
                if ($par[1] < 256 && $par[2] < 256 && $par[3] < 256 && $par[4] < 256) {
                    $return[] = $ip;
                }
            }
        }
        return $return;
    }
}
?>


Теперь осталось внести изменения в ТДС "Интернет-магазин корзина" и в xsl-шаблон.

В ТДС "Интернет-магазин корзина" находим следующий фрагмент:
...
      }

      /* Запоминаем купон */
      $_SESSION['shop_coupon_text'] = to_str($_POST['shop_coupon_text']);

      /* Отображаем форму ввода адреса */
      // Не выбираем show_location, show_city и show_city_area, т.к. подгружаются через AJAX
      $shop->ShowAddress(to_str($GLOBALS['LA']['xsl_delivery_address']), $shop_id, array('show_location' => false, 'show_city' => false, 'show_city_area' => false), $external_propertys);


   }
...
и меняем его на следующий фрагмент:
...
}

      /* Запоминаем купон */
      $_SESSION['shop_coupon_text'] = to_str($_POST['shop_coupon_text']);

       // Определяем город пользователя
        
        @include_once (CMS_FOLDER . '/modules/geoip.class.php');
        if (defined('IPGEO_SERVER')) {
            $user_ip = ($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : getenv("HTTP_X_FORWARDED_FOR");
            $IPGeo = new IPGeo($user_ip);
            $city = trim(to_str($IPGeo->ip($user_ip)));
            if ($city) {
                $cities = $shop->GetAllCity();
                while ($city_row = mysql_fetch_assoc($cities)) {
                    if (strtolower($city_row['shop_city_name']) == strtolower($city)) {
                        $city_id = $city_row['shop_city_id'];
                        $location_id = $city_row['shop_location_id'];
                        break;
                    }
                }
                if (isset($city_id)) {
                    $location_row = $shop->GetLocation($location_id);
                    $country_id = $location_row['shop_country_id'];
                }
            }
        }
        /* Отображаем форму ввода адреса */
        if (isset($city_id)) {
            $external_propertys['city_id'] = $city_id;
            $external_propertys['location_id'] = $location_id;
            $external_propertys['country_id'] = $country_id;
            $shop->ShowAddress(to_str($GLOBALS['LA']['xsl_delivery_address']), $shop_id,
                array('show_location' => true, 'show_city' => true, 'show_city_area' => true), $external_propertys);
        } else {
            // Не выбираем show_location, show_city и show_city_area, т.к. подгружаются через AJAX
            $shop->ShowAddress(to_str($GLOBALS['LA']['xsl_delivery_address']), $shop_id,
                array('show_location' => false, 'show_city' => false, 'show_city_area' => false),
                $external_propertys);
        }


}
...


После этого меняем код xsl-шаблона "МагазинАдресДоставки" на следующий:
<?xml version="1.0" encoding="windows-1251"?>
<!DOCTYPE xsl:stylesheet>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output xmlns="http://www.w3.org/TR/xhtml1/strict" doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" encoding="Windows-1251" indent="yes" method="html" omit-xml-declaration="no" version="1.0" media-type="text/xml"/>

   <xsl:template match="/locations">
      <!-- Строка шага заказа -->
      <ul class="shop_navigation gray">
         <li class="shop_navigation_current">
            <span>Адрес доставки</span>&#x2192;</li>
         <li>
            <span>Способ доставки</span>&#x2192;</li>
         <li>
            <span>Форма оплаты</span>&#x2192;</li>
         <li>
            <span>Данные доставки</span>
         </li>
      </ul>

      <SCRIPT type="text/javascript" language="JavaScript">
         <xsl:comment>
            <xsl:text disable-output-escaping="yes">
               <![CDATA[
               location_select_id = "location";
               city_select_id = "sel_city";
               cityarea_select_id = "sel_city_area";
               ]]>
            </xsl:text>
         </xsl:comment>
      </SCRIPT>

      <xsl:variable name="country_id">
         <xsl:choose>
            <xsl:when test="/locations/external_propertys/country_id/node()">
               <xsl:value-of select="/locations/external_propertys/country_id"/>
            </xsl:when>
            <xsl:otherwise>
               <xsl:value-of select="/locations/country[@select = 1]/@id"/>
            </xsl:otherwise>
         </xsl:choose>
      </xsl:variable>


      <form name="address" id="address" method="POST">
         <h1>Адрес доставки</h1>
         <p>
            <a href="{shop/path}cart/">Корзина</a>
         </p>
         <table>
            <tr>
               <td>Страна:</td>
               <td>
                  <select id="country" style="width: 390px;" name="country" onchange="doSetLocation(this.options[this.selectedIndex].value, '{/locations/shop/path}cart/')">
                     <option value="0">..</option>
                     <xsl:apply-templates select="country"/>
                  </select>
                  <span class="red_star" style="position: relative; top: 4px;">*</span>
               </td>
            </tr>

            <tr>
               <td>Область:</td>
               <td>
                  <select name="location" style="width: 390px;" id="location" onchange="doSetCity(this.options[this.selectedIndex].value, '{/locations/shop/path}cart/')">
                     <option value="0">..</option>
                     <xsl:apply-templates select="location[@parent = $country_id]"/>
                  </select>
                  <span class="red_star" style="position: relative; top: 4px;">*</span>
               </td>
            </tr>
            <tr>
               <td>Город:</td>
               <td>
                  <select name="sel_city" style="width: 390px;" id="sel_city" onchange="doSetCityArea(this.options[this.selectedIndex].value, '{/locations/shop/path}cart/')">
                     <option value="0">..</option>
                     <xsl:variable name="location_id">
                        <xsl:choose>
                           <xsl:when test="/locations/external_propertys/location_id/node()">
                              <xsl:value-of select="/locations/external_propertys/location_id"/>
                           </xsl:when>
                           <xsl:otherwise>
                              <xsl:value-of select="location[@parent = $country_id]/@id"/>
                           </xsl:otherwise>
                        </xsl:choose>
                     </xsl:variable>                     
                     <xsl:apply-templates select="city[@parent = $location_id]"/>
                  </select>
               </td>
            </tr>
            <tr>
               <td>Район города:</td>
               <td>
                  <select name="sel_city_area" style="width: 390px;" id="sel_city_area">
                     <option value="0">..</option>
                  </select>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">Индекс:</td>
               <td>
                  <input type="text" size="5" class="large_input" style="width: 90px;" name="index" value="{external_propertys/site_users_postcode}"/>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">Улица, дом, квартира:<br/>
               (город, район, если не выбраны)</td>
               <td>
                  <input type="text" size="30" class="large_input" style="width: 390px;" name="full_address" value="{external_propertys/site_users_address}"/>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">Фамилия, Имя, Отчество:</td>
               <td>
                  <input type="text" size="30" class="large_input" style="width: 124px; margin-right: 5px;" name="site_users_surname" value="{external_propertys/site_users_surname}"/>
                  <input type="text" size="30" class="large_input" style="width: 124px; margin-right: 5px;" name="site_users_name" value="{external_propertys/site_users_name}"/>
                  <input type="text" size="30" class="large_input" style="width: 124px; margin-right: 5px;" name="site_users_patronymic" value="{external_propertys/site_users_patronymic}"/>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">Компания:</td>
               <td>
                  <input type="text" size="30" class="large_input" style="width: 390px;" name="site_users_company" value="{external_propertys/site_users_company}"/>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">Телефон:</td>
               <td>
                  <input type="text" size="30" class="large_input" style="width: 390px;" name="site_users_phone" value="{external_propertys/site_users_phone}"/>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">Факс:</td>
               <td>
                  <input type="text" size="30" class="large_input" style="width: 390px;" name="site_users_fax" value="{external_propertys/site_users_fax}"/>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">E-mail:</td>
               <td>
                  <input type="text" size="30" class="large_input" style="width: 390px;" name="site_users_email" value="{external_propertys/site_users_email}"/>
               </td>
            </tr>
            <tr>
               <td style="vertical-align: middle;">Комментарий к заказу:</td>
               <td>
                  <textarea rows="2" class="large_input" style="width: 390px;" name="description"></textarea>
               </td>
            </tr>
            <tr>
               <td>
                  <div class="gray_button">
                     <div>
                        <input name="step_2" value="Далее &#x2192;" type="submit"></input>
                     </div>
                  </div>
               </td>
            </tr>
         </table>
      </form>

      <!-- Автоматически заполняем все дочерние элементы страны, если город пользователя не определился по GeoIP-->
      <xsl:if test="not(/locations/external_propertys/country_id/node())" >
      <SCRIPT type="text/javascript" language="JavaScript">var oldHandler=window['onload'];
         window['onload']=function(){if(typeof(oldHandler)=='function'){oldHandler();}newHandler();};
         function newHandler(){
           doSetLocation(document.getElementById('country').options[document.getElementById('country').selectedIndex].value, '<xsl:value-of select="/locations/shop/path"/>cart/');
         }</SCRIPT>
      </xsl:if>
   </xsl:template>

   <!-- Шаблон заполняет options для стран -->
   <xsl:template match="country">
      <xsl:choose>
         <!-- Если страна задана по умолчанию -->
         <xsl:when test="(not(/locations/external_propertys/country_id/node()) and @select=1) or (/locations/external_propertys/country_id/node() and @id = /locations/external_propertys/country_id)">
            <option value="{@id}" selected="selected" style="font-weight: bold;">
               <xsl:value-of disable-output-escaping="yes" select="name"/>
            </option>
         </xsl:when>
         <xsl:otherwise>
            <option value="{@id}">
               <xsl:value-of disable-output-escaping="yes" select="name"/>
            </option>
         </xsl:otherwise>
      </xsl:choose>
   </xsl:template>

   <!-- Шаблон заполняет options для местоположений (областей) -->
   <xsl:template match="location">
      <xsl:choose>
         <!-- Если страна задана по умолчанию -->
         <xsl:when test="/locations/external_propertys/location_id/node() and @id = /locations/external_propertys/location_id">
            <option value="{@id}" selected="selected" style="font-weight: bold;">
               <xsl:value-of disable-output-escaping="yes" select="name"/>
            </option>
         </xsl:when>
         <xsl:otherwise>
            <option value="{@id}">
               <xsl:value-of disable-output-escaping="yes" select="name"/>
            </option>
         </xsl:otherwise>
      </xsl:choose>
   </xsl:template>

   <!-- Шаблон заполняет options для городов -->
   <xsl:template match="city">
      <xsl:choose>
         <!-- Если страна задана по умолчанию -->
         <xsl:when test="/locations/external_propertys/city_id/node() and @id = /locations/external_propertys/city_id">
            <option value="{@id}" selected="selected" style="font-weight: bold;">
               <xsl:value-of disable-output-escaping="yes" select="name"/>
            </option>
         </xsl:when>
         <xsl:otherwise>
            <option value="{@id}">
               <xsl:value-of disable-output-escaping="yes" select="name"/>
            </option>
         </xsl:otherwise>
      </xsl:choose>
   </xsl:template>

   <!-- Шаблон заполняет options для районов -->
   <xsl:template match="cityarea">
      <option value="{@id}">
         <xsl:value-of disable-output-escaping="yes" select="name"/>
      </option>
   </xsl:template>
</xsl:stylesheet>


И пользуемся

Потестировать технологию какое-то время можно будет оформляя заказы здесь - http://test.pskelectro.ru/
У меня однозначно определяется Санкт-Петербург

P.S. Прошу не обращать внимания на некоторый бардак на сайте - это просто тестовая площадка для моих экспериментов.
Заказов не беру. Консультирую редко.
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
Добавлю еще несколько слов в качестве просьбы к разработчикам системы.

Вот мне лично в приведенном выше коде не нравится кусок, который преобразует название города, возвращаемое сервисом, во внутренний id города в контексте HostCMS:
$cities = $shop->GetAllCity();
while ($city_row = mysql_fetch_assoc($cities)) {
if (strtolower($city_row['shop_city_name']) == strtolower($city)) {
и так далее
- как-то не рационально перебирать все города пока не найдется нужный, да и на хайлоад-проектах может сказаться негативно.
Проблему можно было-бы обойти, сделав в этом месте прямой запрос к БД, но я считаю что не стоит пользоваться этим способом, без совершенно крайней на то необходимости.
Поэтому просьба - нельзя ли в классе shop добавить набор методов выбора географических объектов по их названиям, а не только по id как есть сейчас? То есть что-нибудь типа GetCityByName(), GetLocationByName() ну и т.д.
Заказов не беру. Консультирую редко.
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
Kotoff,
решил попробовать поставить, но на http://test.pskelectro.ru/ у меня Питер не определяется У меня все тоже многоточие)
Но если в целом эта штука заработает - то вам огромное спасибо за код!
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
ironwayru, а вы сами откуда, и какой у вас ip? Описанное выше может не работать в двух случаях - когда не определился город, и если написание названия города в сервисе не совпадает с таковым в hostcms (например, опечатка).
Первая причина определяется очень просто - достаточно зайти сюда http://ipgeobase.ru/ и посмотреть, определится ли город по вашему ip в самом сервисе. Если нет - то тут уже от меня ничего не зависит, нужно писать в сам сервис, что я вот в таком городе, у меня вот такой ip, а ваш сервис меня не находит.
А если сервис ваш город определяет, а магазин - нет, тогда пишите что за город у вас такой, и ip ваш тоже напишите, буду смотреть в чем дело
Заказов не беру. Консультирую редко.
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
Ну и парочку картинок, в качестве пруфа:

Определение города в сервисе - http://floomby.ru/content/v2LDiTKPEK/

И определение города в корзине - http://floomby.ru/content/D0CLwN3wEm/
Заказов не беру. Консультирую редко.
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
Потестил сервис:

Питер, оператор Корбина

сервис ipgeobase.ru
ваш: ip *****
Ваш город:      Москва
Ваш регион:    Москва
Ваш округ:      Центральный


ку
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
Kotoff писал(а):
ironwayru, а вы сами откуда, и какой у вас ip?

Ну я там выше написал, что Питер у меня не определяется Я из Питера
Но проблема у меня, точнее у конторы - айпи определяется как чешский))

Но я себе такую тему все равно поставлю, т.к. база у них обновляется регулярно, и я думаю, что, как правило, ай-пи будет определяться правильно
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
ironwayru, а кто у вас провайдер? На http://ipgeobase.ru/ слева под определившимся городом есть ссылочка - "Неправильно? Сообщите нам!" - так и сообщите им!

compaq, ага, бывает такая проблема с Корбиной, у меня у самого выделенный домашний ip от Корбины на одних сайтах определяется как Питер, на других как Москва. Хотя что интересно, ipgeobase у меня дома Питер определяет правильно. У вас видимо ip из другого пула, или может быть подключаетесь как-то иначе, через другой VPN.
Но в любом случае есть есть смысл сообщить об этом в сервис и вам
Заказов не беру. Консультирую редко.
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
Kotoff писал(а):
так и сообщите им!

Сообщил
Провайдер Инфо-Лан. Кстати, провайдер на разных сервисах определяется правильно)

Проблема сейчас в том, что после замены шаблона "АдресДоставки" у меня вобще пустая страница отображается) Не пойму пока, в чем дело... Причем даже правый блок и подвал не загружаются... И дело даже не в шаблоне - может в ТДС что не так...
#
Re: Автоопределение города пользователя при оформлении заказа (на основе GeoIP)
В ТДС правки внесли невнимательно, либо стерли нужную фигурную скобку, либо наборот, поставили.
Заказов не беру. Консультирую редко.
Авторизация