v1.0.178: zip_source — БД, Python, JS. Дедупликация по паре (zip_source, name). addZipFile передаёт имя архива.
This commit is contained in:
@@ -13,6 +13,7 @@ db_prompts.seed_defaults()
|
|||||||
# Schema migration: classify_raw column (idempotent)
|
# Schema migration: classify_raw column (idempotent)
|
||||||
execute("ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_raw text")
|
execute("ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_raw text")
|
||||||
execute("ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_input text")
|
execute("ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_input text")
|
||||||
|
execute("ALTER TABLE documents ADD COLUMN IF NOT EXISTS zip_source text")
|
||||||
|
|
||||||
# ── DB modules ────────────────────────────────────────────────────────────
|
# ── DB modules ────────────────────────────────────────────────────────────
|
||||||
from db import supplements as db_supplements
|
from db import supplements as db_supplements
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
from .connection import query, execute, execute_returning
|
from .connection import query, execute, execute_returning
|
||||||
|
|
||||||
|
|
||||||
def insert(filename, mime_type, original_bytes, status="uploaded", batch_id=None):
|
def insert(filename, mime_type, original_bytes, status="uploaded", batch_id=None, zip_source=None):
|
||||||
"""Insert document, return row dict."""
|
"""Insert document, return row dict."""
|
||||||
return execute_returning(
|
return execute_returning(
|
||||||
"""INSERT INTO documents (filename, mime_type, original_bytes, status, batch_id)
|
"""INSERT INTO documents (filename, mime_type, original_bytes, status, batch_id, zip_source)
|
||||||
VALUES (%s, %s, %s, %s, %s) RETURNING *""",
|
VALUES (%s, %s, %s, %s, %s, %s) RETURNING *""",
|
||||||
(filename, mime_type, original_bytes, status, batch_id),
|
(filename, mime_type, original_bytes, status, batch_id, zip_source),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -73,7 +73,8 @@ def list_by_batch(batch_id):
|
|||||||
"""All documents in a batch with classification fields."""
|
"""All documents in a batch with classification fields."""
|
||||||
return query(
|
return query(
|
||||||
"""SELECT id, filename, status, doc_type, own_number, parent_number,
|
"""SELECT id, filename, status, doc_type, own_number, parent_number,
|
||||||
doc_date, counterparty, classify_status, error_message, classify_raw, classify_input
|
doc_date, counterparty, classify_status, error_message, classify_raw, classify_input,
|
||||||
|
zip_source
|
||||||
FROM documents WHERE batch_id=%s ORDER BY created_at""",
|
FROM documents WHERE batch_id=%s ORDER BY created_at""",
|
||||||
(batch_id,),
|
(batch_id,),
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-7
@@ -170,7 +170,7 @@ window.toggleClassifyDetail = async function(i) {
|
|||||||
* - Возвращает Promise<ответ API> с полями doc_id, contract_id, parsed
|
* - Возвращает Promise<ответ API> с полями doc_id, contract_id, parsed
|
||||||
* - Таймаут 180с (большие PDF)
|
* - Таймаут 180с (большие PDF)
|
||||||
*/
|
*/
|
||||||
function uploadFile(file, onProgress) {
|
function uploadFile(file, onProgress, zipSource) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
// .doc → конвертация в docx (старый формат Word)
|
// .doc → конвертация в docx (старый формат Word)
|
||||||
var isDoc = file.name.toLowerCase().endsWith('.doc') && !file.name.toLowerCase().endsWith('.docx');
|
var isDoc = file.name.toLowerCase().endsWith('.doc') && !file.name.toLowerCase().endsWith('.docx');
|
||||||
@@ -183,6 +183,7 @@ function uploadFile(file, onProgress) {
|
|||||||
fd.append('files', uploadFile, uploadName);
|
fd.append('files', uploadFile, uploadName);
|
||||||
if (state.contractId) fd.append('contract_id', state.contractId);
|
if (state.contractId) fd.append('contract_id', state.contractId);
|
||||||
fd.append('batch_id', state.batchId);
|
fd.append('batch_id', state.batchId);
|
||||||
|
if (zipSource) fd.append('zip_source', zipSource);
|
||||||
xhr.open('POST', UPLOAD_URL);
|
xhr.open('POST', UPLOAD_URL);
|
||||||
xhr.upload.onprogress = function(e) {
|
xhr.upload.onprogress = function(e) {
|
||||||
if (e.lengthComputable && onProgress) onProgress(Math.round(e.loaded / e.total * 100));
|
if (e.lengthComputable && onProgress) onProgress(Math.round(e.loaded / e.total * 100));
|
||||||
@@ -340,7 +341,7 @@ async function addZipFile(file) {
|
|||||||
var mime = {docx:'application/vnd.openxmlformats-officedocument.wordprocessingml.document',doc:'application/msword',pdf:'application/pdf'}[zf.ext] || 'application/octet-stream';
|
var mime = {docx:'application/vnd.openxmlformats-officedocument.wordprocessingml.document',doc:'application/msword',pdf:'application/pdf'}[zf.ext] || 'application/octet-stream';
|
||||||
var extractedFile = new File([bytes], zf.filename, { type: mime, lastModified: Date.now() });
|
var extractedFile = new File([bytes], zf.filename, { type: mime, lastModified: Date.now() });
|
||||||
|
|
||||||
await addRegularFile(extractedFile);
|
await addRegularFile(extractedFile, file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 3: убрать временную строку ZIP (только после успешной обработки всех файлов)
|
// Шаг 3: убрать временную строку ZIP (только после успешной обработки всех файлов)
|
||||||
@@ -365,14 +366,16 @@ async function addZipFile(file) {
|
|||||||
*
|
*
|
||||||
* Мутирует state.files, вызывает render(state) после каждого изменения.
|
* Мутирует state.files, вызывает render(state) после каждого изменения.
|
||||||
*/
|
*/
|
||||||
async function addRegularFile(file) {
|
async function addRegularFile(file, zipSource) {
|
||||||
// Создать запись с начальным статусом
|
// Создать запись с начальным статусом
|
||||||
var entry = { name: file.name, lastModified: file.lastModified, size: file.size, file: file, status: { kind: 'uploading', pct: 0 } };
|
var entry = { name: file.name, lastModified: file.lastModified, size: file.size, file: file, status: { kind: 'uploading', pct: 0 }, zip_source: zipSource || null };
|
||||||
|
|
||||||
// Заменить существующий файл с тем же именем или добавить новый
|
// ДЕДУПЛИКАЦИЯ: ключ = (zip_source, name), чтобы одноимённые файлы из разных ZIP не затирались
|
||||||
|
var dupKey = (zipSource || '') + '/' + file.name;
|
||||||
var existingIdx = -1;
|
var existingIdx = -1;
|
||||||
for (var j = 0; j < state.files.length; j++) {
|
for (var j = 0; j < state.files.length; j++) {
|
||||||
if (state.files[j].name === file.name) { existingIdx = j; break; }
|
var ejKey = (state.files[j].zip_source || '') + '/' + state.files[j].name;
|
||||||
|
if (ejKey === dupKey) { existingIdx = j; break; }
|
||||||
}
|
}
|
||||||
var rowIdx;
|
var rowIdx;
|
||||||
if (existingIdx >= 0) {
|
if (existingIdx >= 0) {
|
||||||
@@ -393,7 +396,7 @@ async function addRegularFile(file) {
|
|||||||
var resp = await uploadFile(file, function(pct) {
|
var resp = await uploadFile(file, function(pct) {
|
||||||
state.files[rowIdx].status = { kind: 'uploading', pct: pct };
|
state.files[rowIdx].status = { kind: 'uploading', pct: pct };
|
||||||
render(state);
|
render(state);
|
||||||
});
|
}, zipSource);
|
||||||
// Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys)
|
// Бэкенд возвращает doc_id и contract_id (нижний регистр — Python keys)
|
||||||
if (resp && resp.contract_id) state.contractId = resp.contract_id;
|
if (resp && resp.contract_id) state.contractId = resp.contract_id;
|
||||||
state.files[rowIdx].doc_id = resp.doc_id;
|
state.files[rowIdx].doc_id = resp.doc_id;
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ def handle_upload(rfile, content_type, content_length):
|
|||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
batch_id = None
|
batch_id = None
|
||||||
|
|
||||||
|
# zip_source — имя родительского ZIP-архива (для визуальной группировки)
|
||||||
|
zip_source = None
|
||||||
|
if "zip_source" in fs:
|
||||||
|
raw_zip = fs.getfirst("zip_source", "")
|
||||||
|
if raw_zip:
|
||||||
|
zip_source = os.path.basename(raw_zip)[:255] # защита от path traversal + лимит
|
||||||
|
|
||||||
if not filename or not file_data:
|
if not filename or not file_data:
|
||||||
return {"ok": False, "error": "no file in request"}
|
return {"ok": False, "error": "no file in request"}
|
||||||
|
|
||||||
@@ -71,7 +78,7 @@ def handle_upload(rfile, content_type, content_length):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass # old record may not exist or FK issue — proceed with insert
|
pass # old record may not exist or FK issue — proceed with insert
|
||||||
|
|
||||||
doc = documents.insert(filename, mime, b64, batch_id=batch_id)
|
doc = documents.insert(filename, mime, b64, batch_id=batch_id, zip_source=zip_source)
|
||||||
|
|
||||||
if not contract_id:
|
if not contract_id:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
*
|
*
|
||||||
* state.files — бывший fileQueue, массив загруженных файлов.
|
* state.files — бывший fileQueue, массив загруженных файлов.
|
||||||
* Каждый элемент: { name, size, lastModified, file, doc_id,
|
* Каждый элемент: { name, size, lastModified, file, doc_id,
|
||||||
* uploaded, parsed, parseInfo, supp_id, supp_type, status }
|
* uploaded, parsed, parseInfo, supp_id, supp_type, status, zip_source }
|
||||||
*
|
*
|
||||||
* state.contractId — бывший contractId, ID контракта в БД.
|
* state.contractId — бывший contractId, ID контракта в БД.
|
||||||
* Приходит с бэкенда после первого успешного upload.
|
* Приходит с бэкенда после первого успешного upload.
|
||||||
|
|||||||
Reference in New Issue
Block a user