| с периодичностью запускается агент, если есть csv файл создает контакты и сделки, удаляет файл Есть
|
use Bitrix\Crm\FieldMultiTable;
use Bitrix\Main\Loader;
class ParserPatientCsv
{
private const FILE_PATH = "/upload/import_file.csv";
private const SEPARATOR = ',';
private const LIST_PROPS = [
'UF_CRM_1761210004' => 114, //114
'UF_CRM_1761210195' => 116, //116
'UF_CRM_1761210342' => 118, //118
'UF_CRM_1761210413' => 119, //119
'UF_CRM_1761743809' => 121, //121
];
private const MAPPING = [
'Центр' => 'UF_CRM_1761210004',
'Специальность' => 'UF_CRM_1761210195',
'Страховая' => 'UF_CRM_1761210342',
'Услуга' => 'UF_CRM_1761210413',
'Статус_согласования_услуги' => 'UF_CRM_1761743809',
'Врач' => 'UF_CRM_1761901380',
'Код_диагноза' => 'UF_CRM_1761901400',
'Номер_Истории_болезни' => 'UF_CRM_1761744904419',
'Пациент' => 'UF_CRM_1761746907',
'Телефон' => 'UF_CRM_1761746943',
'Комментарий_к_записи' => 'UF_CRM_1761743834',
'Результат_взаимодействия_с_пациентом' => 'UF_CRM_1761743868',
'Дата_создания' => 'UF_CRM_1761743940210',
'Ссылка' => 'UF_CRM_1761210451',
];
private const DEAL_DEFAULT = [
'ASSIGNED_BY_ID' => 9,
'CATEGORY_ID' => 0,
'STAGE_ID' => 'NEW'
];
private const OPTIONS = [
'CURRENT_USER' => 1
];
private const BP_ID = 9;
private array $propertyValues = [];
private array $contacts = [];
private array $headers = [];
private array $lines = [];
private array $newDealsIds = [];
private array $newContactIds = [];
private array $errors = [];
public function parse(): void
{
if (!Loader::includeModule("crm")) {
self::addToLog('error', 'error include crm module');
return;
}
if (!$this->readFile()) {
self::addToLog('error', 'error read file');
return;
}
if (!$this->parseHeaders()) {
self::addToLog('error', 'error parse headers');
return;
}
$this->fillContacts();
$this->fillListProps();
$this->addDeals();
self::addToLog('Результат обработки CSV', self::formatFinishMessage());
$this->deleteFile();
}
public function parseTest(): void
{
if (!$this->readFile()) {
self::addToLogTest('error', 'error read file');
return;
}
echo '<pre>';
print_r($this->lines);
echo '<pre>';
if (!$this->parseHeaders()) {
self::addToLogTest('error', 'error parse headers');
return;
}
$this->addDealsTest();
self::addToLogTest('Результат обработки CSV', self::formatFinishMessage());
}
private function readFile(): bool
{
$filePath = $_SERVER["DOCUMENT_ROOT"] . self::FILE_PATH;
if (!file_exists($filePath)) {
return false;
}
$content = file_get_contents($filePath);
$this->lines = $this->parseCsvLines($content);
return count($this->lines) > 1;
}
private function parseCsvLines(string $content): array
{
$lines = [];
$currentLine = '';
$inQuotes = false;
$length = strlen($content);
for ($i = 0; $i < $length; $i++) {
$char = $content[$i];
$nextChar = $i + 1 < $length ? $content[$i + 1] : '';
if ($char === '"') {
// Проверяем, это экранированная кавычка или нет
if ($inQuotes && $nextChar === '"') {
$currentLine .= '"';
$i++; // Пропускаем следующую кавычку
} else {
$inQuotes = !$inQuotes;
$currentLine .= $char;
}
} elseif ($char === "\n" && !$inQuotes) {
// Перенос строки вне кавычек - конец строки CSV
$lines[] = $currentLine;
$currentLine = '';
} elseif ($char === "\r" && $nextChar === "\n" && !$inQuotes) {
// Windows перенос строки вне кавычек
$lines[] = $currentLine;
$currentLine = '';
$i++; // Пропускаем \n
} elseif ($char === "\r" && !$inQuotes) {
// Старый Mac перенос строки вне кавычек
$lines[] = $currentLine;
$currentLine = '';
} else {
$currentLine .= $char;
}
}
// Добавляем последнюю строку
if ($currentLine !== '') {
$lines[] = $currentLine;
}
return $lines;
}
private function detectEncoding(string $content): string
{
// Проверяем BOM
if (strncmp($content, "\xEF\xBB\xBF", 3) === 0) {
return 'UTF-8';
}
if (strncmp($content, "\xFF\xFE", 2) === 0) {
return 'UTF-16LE';
}
if (strncmp($content, "\xFE\xFF", 2) === 0) {
return 'UTF-16BE';
}
// Пытаемся определить по содержимому
$encodings = ['UTF-8', 'Windows-1251', 'CP1251', 'ISO-8859-1', 'KOI8-R'];
foreach ($encodings as $encoding) {
if (mb_check_encoding($content, $encoding)) {
return $encoding;
}
}
// По умолчанию считаем Windows-1251 (как было раньше)
return 'Windows-1251';
}
public function parseHeaders(): bool
{
if (empty($this->lines[0])) {
return false;
}
$columns = self::separateLine($this->lines[0]);
$this->headers = [];
foreach ($columns as $index => $header) {
$header = trim($header);
if (isset(self::MAPPING[$header])) {
$this->headers[$index] = $header;
}
}
return !empty($this->headers);
}
private function fillContacts(): void
{
$contactData = $this->parseContactData();
$phones = array_keys($contactData);
if (empty($phones)) {
return;
}
$this->loadExistingContacts($phones);
$this->createMissingContacts($contactData);
}
private function parseContactData(): array
{
$contactData = [];
foreach ($this->lines as $lineNum => $line) {
if ($lineNum == 0) {
continue;
}
$data = self::separateLine($line);
$phone = '';
$patientName = '';
foreach ($this->headers as $index => $header) {
if (!isset($data[$index])) {
continue;
}
$value = trim($data[$index]);
if ($header === 'Телефон' && !empty($value)) {
$phone = $value;
} elseif ($header === 'Пациент' && !empty($value)) {
$patientName = $value;
}
}
if (!empty($phone)) {
$contactData[$phone] = $patientName;
}
}
return $contactData;
}
private function loadExistingContacts(array $phones): void
{
$dbPhones = FieldMultiTable::getList([
'select' => ['ELEMENT_ID', 'VALUE'],
'filter' => [
'ENTITY_ID' => 'CONTACT',
'TYPE_ID' => 'PHONE',
'VALUE' => $phones
],
]);
while ($phone = $dbPhones->fetch()) {
$this->contacts[$phone['VALUE']] = (int)$phone['ELEMENT_ID'];
}
}
private function createMissingContacts(array $phoneToNameMap): void
{
$phonesToCreate = array_diff(array_keys($phoneToNameMap), array_keys($this->contacts));
if (empty($phonesToCreate)) {
return;
}
foreach ($phonesToCreate as $phone) {
$name = $phoneToNameMap[$phone];
$contactFields = [
'NAME' => $name,
'OPENED' => 'Y',
'ASSIGNED_BY_ID' => self::DEAL_DEFAULT['ASSIGNED_BY_ID'],
'TYPE_ID' => 'CLIENT',
'SOURCE_ID' => 'OTHER',
'FM' => [
'PHONE' => [
['VALUE' => $phone, 'VALUE_TYPE' => 'WORK']
]
]
];
$contact = new CCrmContact(false);
$contactId = $contact->Add($contactFields, true, ['CURRENT_USER' => self::OPTIONS['CURRENT_USER']]);
if ($contactId > 0) {
$this->contacts[$phone] = $contactId;
$this->newContactIds[] = $contactId;
} else {
$this->errors[] = $contact->LAST_ERROR ?? '';
}
}
}
private function fillListProps(): void
{
// 1. Сначала загружаем существующие значения списков
foreach (self::LIST_PROPS as $propertyCode => $propId) {
$this->propertyValues[$propertyCode] = [];
$obEnum = new \CUserFieldEnum;
$rsEnum = $obEnum->GetList([], ["USER_FIELD_ID" => $propId]);
while ($arEnum = $rsEnum->Fetch()) {
$this->propertyValues[$propertyCode][$arEnum["ID"]] = $arEnum["VALUE"];
}
}
// 2. Собираем ВСЕ уникальные значения из CSV файла для каждого списка
$valuesFromCsv = [];
foreach (self::LIST_PROPS as $propertyCode => $propId) {
$valuesFromCsv[$propertyCode] = [];
}
foreach ($this->lines as $lineNum => $line) {
if ($lineNum == 0) {
continue; // Пропускаем заголовок
}
$data = self::separateLine($line);
foreach ($this->headers as $index => $header) {
if (!isset($data[$index])) {
continue;
}
$value = trim($data[$index]);
if (empty($value)) {
continue;
}
$fieldCode = self::MAPPING[$header];
// Если это поле списка
if (isset(self::LIST_PROPS[$fieldCode])) {
$valuesFromCsv[$fieldCode][] = $value;
}
}
}
// 3. Убираем дубликаты
foreach ($valuesFromCsv as $propertyCode => $csvValues) {
$valuesFromCsv[$propertyCode] = array_unique($csvValues);
}
// 4. Для каждого списка добавляем отсутствующие значения
foreach ($valuesFromCsv as $propertyCode => $csvValues) {
$propId = self::LIST_PROPS[$propertyCode];
$existingValues = array_map('trim', $this->propertyValues[$propertyCode]);
// Находим значения, которых нет в системе
$valuesToAdd = array_diff($csvValues, $existingValues);
if (!empty($valuesToAdd)) {
$this->addEnumValues($propId, $valuesToAdd, $propertyCode);
}
}
}
private function addEnumValues(int $fieldId, array $values, string $propertyCode): void
{
$obEnum = new \CUserFieldEnum;
$enumValues = [];
$i = 0;
foreach ($values as $value) {
$enumValues["n{$i}"] = ["VALUE" => $value];
$i++;
}
$result = $obEnum->SetEnumValues($fieldId, $enumValues);
if ($result) {
// Обновляем кэш, загружая добавленные значения
$rsEnum = $obEnum->GetList([], ["USER_FIELD_ID" => $fieldId]);
while ($arEnum = $rsEnum->Fetch()) {
if (!isset($this->propertyValues[$propertyCode][$arEnum["ID"]])) {
$this->propertyValues[$propertyCode][$arEnum["ID"]] = $arEnum["VALUE"];
}
}
self::addToLog('Добавлены значения списка',
"В список {$propertyCode} (ID: {$fieldId}) добавлены значения: " . implode(', ', $values));
} else {
$this->errors[] = "Не удалось добавить значения в список {$propertyCode}: " . implode(', ', $values);
}
}
private function getListValue(string $propertyCode, string $value): ?int
{
$value = trim($value);
if (empty($value)) {
return null;
}
foreach ($this->propertyValues[$propertyCode] as $valueId => $propertyValue) {
if (trim($propertyValue) == $value) {
return $valueId;
}
}
return null;
}
private function addDeals(): void
{
$deal = new CCrmDeal(false);
foreach ($this->lines as $lineNum => $line) {
if ($lineNum == 0) {
continue;
}
try {
$dealFields = $this->fillDealFields($line);
if (empty($dealFields[self::MAPPING['Дата_создания']])) {
//защита от пустой строки
continue;
}
$dealId = $deal->Add($dealFields, true, self::OPTIONS);
if ($dealId > 0) {
$updateFields = ['TITLE' => 'Сделка №' . $dealId . ' ' . ($dealFields['UF_CRM_1761746907'] ?? '')];
$deal->Update($dealId, $updateFields, true, self::OPTIONS);
$this->newDealsIds[] = $dealId;
if (CModule::IncludeModule('bizproc')) {
$arErrors = [];
CBPDocument::StartWorkflow(
self::BP_ID,
["crm", "CCrmDocumentDeal", "DEAL_" . $dealId],
["TargetUser" => "user_" . self::OPTIONS['CURRENT_USER']],
$arErrors
);
}
} else {
$errorMsg = $deal->LAST_ERROR ?: 'Неизвестная ошибка';
$this->errors[] = 'Не добавили строчку ' . $lineNum . ': ' . $errorMsg;
}
} catch (Exception $e) {
$this->errors[] = 'Строка ' . $lineNum . ': ' . $e->getMessage();
}
}
}
private function addDealsTest(): void
{
foreach ($this->lines as $lineNum => $line) {
if ($lineNum == 0) {
continue;
}
try {
$dealFields = $this->fillDealFields($line);
echo '<pre>';
print_r(self::separateLine($line));
print_r($dealFields);
echo '</pre>';
} catch (Exception $e) {
$this->errors[] = 'Строка ' . $lineNum . ': ' . $e->getMessage();
}
}
}
private function fillDealFields(string $line): array
{
$arLine = self::separateLine($line);
$arDeal = self::DEAL_DEFAULT;
$arDeal['TITLE'] = 'Сделка № (ID будет добавлен)';
foreach ($this->headers as $index => $header) {
if (!isset($arLine[$index])) {
continue;
}
$value = trim($arLine[$index]);
if (empty($value)) {
continue;
}
$fieldCode = self::MAPPING[$header];
if (isset(self::LIST_PROPS[$fieldCode])) {
$listValueId = $this->getListValue($fieldCode, $value);
if ($listValueId) {
$arDeal[$fieldCode] = $listValueId;
} else {
//$arDeal[$fieldCode] = 'test: '.$value;
}
} elseif ($header === 'Дата_создания') {
$timestamp = strtotime($value);
if ($timestamp !== false) {
$arDeal[$fieldCode] = ConvertTimeStamp($timestamp, 'FULL');
}
} elseif ($header === 'Телефон') {
$arDeal['CONTACT_ID'] = $this->contacts[$value] ?? null;
$arDeal[$fieldCode] = $value;
} else {
$arDeal[$fieldCode] = $value;
}
}
return $arDeal;
}
private function deleteFile(): void
{
$filePath = $_SERVER["DOCUMENT_ROOT"] . self::FILE_PATH;
if (file_exists($filePath)) {
unlink($filePath);
}
}
private function formatFinishMessage(): string
{
$message = "Обработка CSV завершена.\n\n";
$message .= "Успешно создано сделок: " . count($this->newDealsIds) . "\n";
$message .= "Успешно создано контактов: " . count($this->newContactIds) . "\n";
if (!empty($this->errors)) {
$message .= "Ошибок: " . count($this->errors) . "\n";
$message .= "Список ошибок:\n" . implode("\n", $this->errors);
}
return $message;
}
private static function separateLine(string $line): array
{
$line = trim($line, "\r\n");
return str_getcsv($line, self::SEPARATOR, '"', '\\');
}
private static function addToLog(string $subject, string $message): void
{
mail('hello@nikaverro.ru', 'ParserPatientCsv: ' . $subject, $message);
}
private static function addToLogTest(string $subject, string $message): void
{
echo $subject.', '.$message;
}
} |
агент в init.php
function parsePatientCsv(): string
{
try{
require($_SERVER['DOCUMENT_ROOT'] .'/bitrix/php_interface/include/parserpatientcsv.php');
$parser = new ParserPatientCsv();
$parser->parse();
} catch (\Exception $e) {
mail('hello@nikaverro.ru', 'error ParserPatientCsv in init ', $e->getMessage());
return '';
}
return 'parsePatientCsv();';
} |