Compare commits
6 Commits
e92964ce71
...
0.2.7.4
| Author | SHA1 | Date | |
|---|---|---|---|
| ecd9933c0f | |||
| 71e34a57f1 | |||
| a0fa32bdba | |||
| 61d2189f21 | |||
| 24d5caef5a | |||
| 415ddc7110 |
@@ -47,16 +47,40 @@ jobs:
|
|||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
SHA: ${{ github.sha }}
|
SHA: ${{ github.sha }}
|
||||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
TOKEN: ${{ gitea.token }}
|
||||||
|
USER_TOKEN: ${{ secrets.token }}
|
||||||
run: |
|
run: |
|
||||||
|
git config --global init.defaultBranch main
|
||||||
if [ -f "$GITHUB_WORKSPACE/Dockerfile" ]; then exit 0; fi
|
if [ -f "$GITHUB_WORKSPACE/Dockerfile" ]; then exit 0; fi
|
||||||
mkdir -p "$GITHUB_WORKSPACE"
|
mkdir -p "$GITHUB_WORKSPACE"
|
||||||
cd "$GITHUB_WORKSPACE"
|
cd "$GITHUB_WORKSPACE"
|
||||||
git init .
|
git init .
|
||||||
if [ -z "$TOKEN" ]; then
|
|
||||||
git fetch --depth=1 "$SERVER/$REPO.git" "$REF"
|
# 优先使用 gitea.token (TOKEN)
|
||||||
|
AUTH_TOKEN="$TOKEN"
|
||||||
|
USE_BASIC_AUTH="false"
|
||||||
|
|
||||||
|
if [ -n "$TOKEN" ]; then
|
||||||
|
echo "Using gitea.token for authentication (Basic Auth)."
|
||||||
|
USE_BASIC_AUTH="true"
|
||||||
|
elif [ -n "$USER_TOKEN" ]; then
|
||||||
|
AUTH_TOKEN="$USER_TOKEN"
|
||||||
|
USE_BASIC_AUTH="true"
|
||||||
|
echo "Using secrets.token for authentication (Basic Auth)."
|
||||||
else
|
else
|
||||||
git -c http.extraHeader="Authorization: Bearer $TOKEN" fetch --depth=1 "$SERVER/$REPO.git" "$REF"
|
echo "Warning: No token found. Attempting unauthenticated fetch (will fail for private repos)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$AUTH_TOKEN" ]; then
|
||||||
|
git fetch --depth=1 "$SERVER/$REPO.git" "$REF"
|
||||||
|
elif [ "$USE_BASIC_AUTH" = "true" ]; then
|
||||||
|
# 使用 Gitea 支持的 Basic Auth: https://x-access-token:token@gitea.domain/user/repo.git
|
||||||
|
# 去掉 SERVER 中的 https:// 或 http:// 前缀以构建正确的 URL
|
||||||
|
CLEAN_SERVER=$(echo "$SERVER" | sed -E 's/^\s*.*:\/\///g')
|
||||||
|
git fetch --depth=1 "https://x-access-token:$AUTH_TOKEN@$CLEAN_SERVER/$REPO.git" "$REF"
|
||||||
|
else
|
||||||
|
# 使用 Bearer Token 进行认证
|
||||||
|
git -c http.extraHeader="Authorization: Bearer $AUTH_TOKEN" fetch --depth=1 "$SERVER/$REPO.git" "$REF"
|
||||||
fi
|
fi
|
||||||
git checkout FETCH_HEAD
|
git checkout FETCH_HEAD
|
||||||
- name: Derive version
|
- name: Derive version
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>注册码申请管理</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #f5f6fa; }
|
|
||||||
.sidebar { position: fixed; top: 0; left: 0; width: 180px; height: 100vh; background: #1e1e2e; color: white; padding: 20px; box-shadow: 2px 0 5px rgba(0,0,0,0.1); z-index: 1000; display: flex; flex-direction: column; align-items: center; }
|
|
||||||
.sidebar h3 { margin-top: 0; font-size: 18px; color: #add8e6; text-align: center; margin-bottom: 20px; }
|
|
||||||
.navigation-links { width: 100%; margin-top: 60px; }
|
|
||||||
.sidebar a { display: block; color: #8be9fd; text-decoration: none; margin: 10px 0; font-size: 16px; padding: 15px; border-radius: 4px; transition: all 0.2s ease; }
|
|
||||||
.sidebar a:hover { color: #ff79c6; background-color: rgba(139, 233, 253, 0.2); }
|
|
||||||
|
|
||||||
.main-content { margin-left: 220px; padding: 40px; }
|
|
||||||
.card { background: #fff; border-radius: 14px; box-shadow: 0 10px 24px rgba(31,35,40,0.08); padding: 24px; }
|
|
||||||
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
|
||||||
.btn { padding: 8px 12px; border: none; border-radius: 10px; cursor: pointer; }
|
|
||||||
.btn-primary { background: #4f46e5; color: #fff; }
|
|
||||||
.btn-secondary { background: #64748b; color: #fff; }
|
|
||||||
.btn-danger { background: #ff4d4f; color: #fff; }
|
|
||||||
.muted { color: #6b7280; font-size: 12px; }
|
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: 12px; }
|
|
||||||
th, td { text-align: left; border-bottom: 1px solid #e5e7eb; padding: 10px 8px; vertical-align: top; font-size: 13px; }
|
|
||||||
tr:hover { background: #f8fafc; }
|
|
||||||
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; background: #eef2ff; color: #3730a3; }
|
|
||||||
.tag.pending { background: #fff7ed; color: #9a3412; }
|
|
||||||
.tag.approved { background: #dcfce7; color: #166534; }
|
|
||||||
.tag.rejected { background: #fee2e2; color: #991b1b; }
|
|
||||||
</style>
|
|
||||||
{% csrf_token %}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="sidebar">
|
|
||||||
<h3>你好,{{ username|default:"管理员" }}</h3>
|
|
||||||
<div class="navigation-links">
|
|
||||||
<a href="{% url 'main:home' %}">返回主页</a>
|
|
||||||
<a id="logoutBtn" style="cursor:pointer;">退出登录</a>
|
|
||||||
<div id="logoutMsg" class="muted" style="margin-top:6px;"></div>
|
|
||||||
{% csrf_token %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-content">
|
|
||||||
<div class="card">
|
|
||||||
<div class="header">
|
|
||||||
<h2 style="margin:0;">注册码申请管理</h2>
|
|
||||||
<div style="display:flex; gap:10px; align-items:center;">
|
|
||||||
<select id="statusFilter" style="padding:8px 10px; border:1px solid #d1d5db; border-radius:10px;">
|
|
||||||
<option value="pending">待审核</option>
|
|
||||||
<option value="">全部</option>
|
|
||||||
<option value="approved">已同意</option>
|
|
||||||
<option value="rejected">已拒绝</option>
|
|
||||||
</select>
|
|
||||||
<button id="refreshBtn" class="btn btn-secondary" type="button">刷新</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="muted">同意后,用户会获得“注册码管理”入口,且仅能使用自己新增的 key。</div>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:120px;">用户</th>
|
|
||||||
<th>申请理由</th>
|
|
||||||
<th style="width:170px;">时间</th>
|
|
||||||
<th style="width:110px;">状态</th>
|
|
||||||
<th style="width:220px;">操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="reqBody"></tbody>
|
|
||||||
</table>
|
|
||||||
<div id="pageMsg" class="muted" style="margin-top:12px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
|
||||||
|
|
||||||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
|
||||||
const msg = document.getElementById('logoutMsg');
|
|
||||||
msg.textContent = '';
|
|
||||||
const csrftoken = getCookie('csrftoken');
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/accounts/logout/', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' },
|
|
||||||
body: JSON.stringify({})
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
|
||||||
if (data.ok) window.location.href = data.redirect_url;
|
|
||||||
} catch (e) { msg.textContent = '登出失败'; }
|
|
||||||
});
|
|
||||||
|
|
||||||
function fmtTime(t){
|
|
||||||
try{
|
|
||||||
const d = new Date(t);
|
|
||||||
if(String(d) !== 'Invalid Date'){
|
|
||||||
const pad = n=> String(n).padStart(2,'0');
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
||||||
}
|
|
||||||
}catch(e){}
|
|
||||||
return t || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStatus(s){
|
|
||||||
const v = String(s || 'pending');
|
|
||||||
const cls = (v === 'approved' || v === 'rejected') ? v : 'pending';
|
|
||||||
const text = v === 'approved' ? '已同意' : (v === 'rejected' ? '已拒绝' : '待审核');
|
|
||||||
return `<span class="tag ${cls}">${text}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRequests(){
|
|
||||||
const status = document.getElementById('statusFilter').value;
|
|
||||||
const msg = document.getElementById('pageMsg');
|
|
||||||
msg.textContent = '加载中...';
|
|
||||||
const url = status ? `/accounts/registration-code/requests/list/?status=${encodeURIComponent(status)}` : '/accounts/registration-code/requests/list/';
|
|
||||||
try{
|
|
||||||
const resp = await fetch(url, { credentials: 'same-origin' });
|
|
||||||
const data = await resp.json();
|
|
||||||
if(!(resp.ok && data && data.ok)){
|
|
||||||
msg.textContent = (data && data.message) ? data.message : '加载失败';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const body = document.getElementById('reqBody');
|
|
||||||
body.innerHTML = '';
|
|
||||||
const rows = data.data || [];
|
|
||||||
if(!rows.length){
|
|
||||||
msg.textContent = '暂无数据';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
msg.textContent = '';
|
|
||||||
rows.forEach(r=>{
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
const uname = (r.username || '') + (r.user_id !== undefined ? `(${r.user_id})` : '');
|
|
||||||
const reason = String(r.reason || '').replace(/</g,'<').replace(/>/g,'>');
|
|
||||||
const created = fmtTime(r.created_at);
|
|
||||||
const statusHtml = renderStatus(r.status);
|
|
||||||
const id = r.request_id || r._id || '';
|
|
||||||
const ops = (String(r.status || 'pending') === 'pending')
|
|
||||||
? `<button class="btn btn-primary" data-act="approve" data-id="${id}">同意</button>
|
|
||||||
<button class="btn btn-danger" data-act="reject" data-id="${id}">拒绝</button>`
|
|
||||||
: `<button class="btn btn-secondary" data-act="view" data-id="${id}">查看</button>`;
|
|
||||||
tr.innerHTML = `<td>${uname}</td><td style="white-space:pre-wrap;">${reason}</td><td>${created}</td><td>${statusHtml}</td><td>${ops}</td>`;
|
|
||||||
body.appendChild(tr);
|
|
||||||
});
|
|
||||||
}catch(e){
|
|
||||||
msg.textContent = '加载失败';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function decide(id, action){
|
|
||||||
const csrftoken = getCookie('csrftoken');
|
|
||||||
const note = '';
|
|
||||||
const resp = await fetch('/accounts/registration-code/requests/decide/', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' },
|
|
||||||
body: JSON.stringify({ request_id: id, action, note })
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
|
||||||
if(!(resp.ok && data && data.ok)){
|
|
||||||
alert((data && data.message) ? data.message : '操作失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadRequests();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('refreshBtn').addEventListener('click', loadRequests);
|
|
||||||
document.getElementById('statusFilter').addEventListener('change', loadRequests);
|
|
||||||
document.addEventListener('click', (e)=>{
|
|
||||||
const t = e.target;
|
|
||||||
if(!(t && t.dataset && t.dataset.id && t.dataset.act)) return;
|
|
||||||
const id = t.dataset.id;
|
|
||||||
const act = t.dataset.act;
|
|
||||||
if(act === 'approve'){
|
|
||||||
if(confirm('确定同意该申请吗?')) decide(id, 'approve');
|
|
||||||
}else if(act === 'reject'){
|
|
||||||
if(confirm('确定拒绝该申请吗?')) decide(id, 'reject');
|
|
||||||
}else if(act === 'view'){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loadRequests();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -307,8 +307,7 @@ def search_data(query):
|
|||||||
"_id": hit.meta.id,
|
"_id": hit.meta.id,
|
||||||
"writer_id": hit.writer_id,
|
"writer_id": hit.writer_id,
|
||||||
"data": hit.data,
|
"data": hit.data,
|
||||||
"image": hit.image,
|
"image": hit.image
|
||||||
"time": getattr(hit, "time", None),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -329,8 +328,7 @@ def search_all():
|
|||||||
"_id": hit.meta.id,
|
"_id": hit.meta.id,
|
||||||
"writer_id": hit.writer_id,
|
"writer_id": hit.writer_id,
|
||||||
"data": hit.data,
|
"data": hit.data,
|
||||||
"image": hit.image,
|
"image": hit.image
|
||||||
"time": getattr(hit, "time", None),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -437,8 +435,7 @@ def search_by_any_field(keyword):
|
|||||||
"_id": hit.meta.id,
|
"_id": hit.meta.id,
|
||||||
"writer_id": hit.writer_id,
|
"writer_id": hit.writer_id,
|
||||||
"data": hit.data,
|
"data": hit.data,
|
||||||
"image": hit.image,
|
"image": hit.image
|
||||||
"time": getattr(hit, "time", None),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -831,8 +828,12 @@ def get_user_by_username(username):
|
|||||||
def get_all_users():
|
def get_all_users():
|
||||||
"""获取所有用户"""
|
"""获取所有用户"""
|
||||||
try:
|
try:
|
||||||
|
search = UserDocument.search()
|
||||||
|
search = search.query("match_all")
|
||||||
|
response = search.execute()
|
||||||
|
|
||||||
users = []
|
users = []
|
||||||
for hit in UserDocument.search().query("match_all").scan():
|
for hit in response:
|
||||||
users.append({
|
users.append({
|
||||||
"user_id": hit.user_id,
|
"user_id": hit.user_id,
|
||||||
"username": hit.username,
|
"username": hit.username,
|
||||||
@@ -844,6 +845,7 @@ def get_all_users():
|
|||||||
"key": list(getattr(hit, 'key', []) or []),
|
"key": list(getattr(hit, 'key', []) or []),
|
||||||
"manage_key": list(getattr(hit, 'manage_key', []) or []),
|
"manage_key": list(getattr(hit, 'manage_key', []) or []),
|
||||||
})
|
})
|
||||||
|
|
||||||
return users
|
return users
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取所有用户失败: {str(e)}")
|
print(f"获取所有用户失败: {str(e)}")
|
||||||
|
|||||||
@@ -81,8 +81,6 @@
|
|||||||
<input type="text" id="searchQuery" class="search-input" placeholder="请输入搜索关键词...">
|
<input type="text" id="searchQuery" class="search-input" placeholder="请输入搜索关键词...">
|
||||||
<select id="keyFilter" class="search-input"></select>
|
<select id="keyFilter" class="search-input"></select>
|
||||||
<button class="btn" onclick="clearKeyFilter()">清空Key筛查</button>
|
<button class="btn" onclick="clearKeyFilter()">清空Key筛查</button>
|
||||||
<select id="typeFilter" class="search-input"></select>
|
|
||||||
<button class="btn" onclick="clearTypeFilter()">清空类型筛查</button>
|
|
||||||
<button class="btn btn-primary" onclick="performSearch('exact')">关键词搜索</button>
|
<button class="btn btn-primary" onclick="performSearch('exact')">关键词搜索</button>
|
||||||
<button class="btn btn-secondary" onclick="performSearch('fuzzy')">模糊搜索</button>
|
<button class="btn btn-secondary" onclick="performSearch('fuzzy')">模糊搜索</button>
|
||||||
<button class="btn" onclick="loadAllData()">显示全部</button>
|
<button class="btn" onclick="loadAllData()">显示全部</button>
|
||||||
@@ -95,31 +93,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_admin or has_manage_key %}
|
|
||||||
<div class="search-container" style="margin-top: 12px;">
|
|
||||||
<div style="font-weight: 600; margin-bottom: 8px;">统计报表</div>
|
|
||||||
<div class="search-controls" style="flex-wrap: wrap;">
|
|
||||||
<input type="datetime-local" id="reportFrom" class="search-input" placeholder="开始时间">
|
|
||||||
<input type="datetime-local" id="reportTo" class="search-input" placeholder="结束时间">
|
|
||||||
<select id="reportInterval" class="search-input">
|
|
||||||
<option value="day">按天</option>
|
|
||||||
<option value="week">按周</option>
|
|
||||||
<option value="month">按月</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-primary" onclick="generateReport()">生成报表</button>
|
|
||||||
<button class="btn" onclick="downloadReportCsv()">下载CSV</button>
|
|
||||||
</div>
|
|
||||||
<div id="reportBox" class="search-result" style="display: none; margin-top: 10px;"></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<table id="dataTable">
|
<table id="dataTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>图片</th>
|
<th>图片</th>
|
||||||
<th>数据</th>
|
<th>数据</th>
|
||||||
<th>时间</th>
|
|
||||||
<th>录入人</th>
|
<th>录入人</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -174,11 +153,6 @@ function getCookie(name) {
|
|||||||
// DOM元素引用
|
// DOM元素引用
|
||||||
const searchQueryInput = document.getElementById('searchQuery');
|
const searchQueryInput = document.getElementById('searchQuery');
|
||||||
const keyFilterSelect = document.getElementById('keyFilter');
|
const keyFilterSelect = document.getElementById('keyFilter');
|
||||||
const typeFilterSelect = document.getElementById('typeFilter');
|
|
||||||
const reportFromInput = document.getElementById('reportFrom');
|
|
||||||
const reportToInput = document.getElementById('reportTo');
|
|
||||||
const reportIntervalSelect = document.getElementById('reportInterval');
|
|
||||||
const reportBox = document.getElementById('reportBox');
|
|
||||||
const searchResultDiv = document.getElementById('searchResult');
|
const searchResultDiv = document.getElementById('searchResult');
|
||||||
const searchStatus = document.getElementById('searchStatus');
|
const searchStatus = document.getElementById('searchStatus');
|
||||||
const searchCount = document.getElementById('searchCount');
|
const searchCount = document.getElementById('searchCount');
|
||||||
@@ -199,9 +173,6 @@ const zoomOutBtn = document.getElementById('zoomOutBtn');
|
|||||||
const resetZoomBtn = document.getElementById('resetZoomBtn');
|
const resetZoomBtn = document.getElementById('resetZoomBtn');
|
||||||
const zoomValue = document.getElementById('zoomValue');
|
const zoomValue = document.getElementById('zoomValue');
|
||||||
|
|
||||||
const IS_ADMIN = {{ is_admin|yesno:"true,false" }};
|
|
||||||
const HAS_MANAGE_KEY = {{ has_manage_key|yesno:"true,false" }};
|
|
||||||
|
|
||||||
// 全局变量
|
// 全局变量
|
||||||
let currentId = '';
|
let currentId = '';
|
||||||
let currentWriter = '';
|
let currentWriter = '';
|
||||||
@@ -211,7 +182,6 @@ let currentSearchQuery = ''; // 记录当前搜索查询
|
|||||||
let isFuzzySearch = false; // 记录当前是否为模糊搜索
|
let isFuzzySearch = false; // 记录当前是否为模糊搜索
|
||||||
let isDeleting = false; // 标记是否正在删除
|
let isDeleting = false; // 标记是否正在删除
|
||||||
let currentKeyFilter = '';
|
let currentKeyFilter = '';
|
||||||
let currentTypeFilter = '';
|
|
||||||
|
|
||||||
// 图片缩放相关变量
|
// 图片缩放相关变量
|
||||||
let currentScale = 1;
|
let currentScale = 1;
|
||||||
@@ -235,10 +205,6 @@ async function performSearch(type) {
|
|||||||
currentKeyFilter = '';
|
currentKeyFilter = '';
|
||||||
if (keyFilterSelect) keyFilterSelect.value = '';
|
if (keyFilterSelect) keyFilterSelect.value = '';
|
||||||
}
|
}
|
||||||
if (currentTypeFilter) {
|
|
||||||
currentTypeFilter = '';
|
|
||||||
if (typeFilterSelect) typeFilterSelect.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSearchQuery = query;
|
currentSearchQuery = query;
|
||||||
isFuzzySearch = type === 'fuzzy';
|
isFuzzySearch = type === 'fuzzy';
|
||||||
@@ -304,11 +270,11 @@ async function loadAllData() {
|
|||||||
showSearchLoading();
|
showSearchLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentKeyFilter || currentTypeFilter) {
|
if (currentKeyFilter) {
|
||||||
const response = await fetch(`/elastic/filter/?key=${encodeURIComponent(currentKeyFilter)}&type=${encodeURIComponent(currentTypeFilter)}`);
|
const response = await fetch(`/elastic/filter-by-key/?key=${encodeURIComponent(currentKeyFilter)}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
displayAllData(data.data || [], currentKeyFilter, currentTypeFilter);
|
displayAllData(data.data || [], currentKeyFilter);
|
||||||
} else {
|
} else {
|
||||||
showSearchMessage(`加载数据失败: ${data.message || '未知错误'}`, 'error');
|
showSearchMessage(`加载数据失败: ${data.message || '未知错误'}`, 'error');
|
||||||
}
|
}
|
||||||
@@ -340,10 +306,7 @@ async function loadAllData() {
|
|||||||
function displayAllData(data, key) {
|
function displayAllData(data, key) {
|
||||||
searchResultDiv.style.display = 'block';
|
searchResultDiv.style.display = 'block';
|
||||||
searchResultDiv.className = 'search-result';
|
searchResultDiv.className = 'search-result';
|
||||||
const labels = [];
|
searchStatus.textContent = key ? `按Key筛查:${key}` : '显示全部数据';
|
||||||
if (key) labels.push(`Key:${key}`);
|
|
||||||
if (currentTypeFilter) labels.push(`类型:${currentTypeFilter}`);
|
|
||||||
searchStatus.textContent = labels.length ? `筛查:${labels.join(',')}` : '显示全部数据';
|
|
||||||
searchCount.textContent = `共 ${data.length} 条记录`;
|
searchCount.textContent = `共 ${data.length} 条记录`;
|
||||||
|
|
||||||
renderTable(data);
|
renderTable(data);
|
||||||
@@ -355,7 +318,7 @@ function clearSearch() {
|
|||||||
searchResultDiv.style.display = 'none';
|
searchResultDiv.style.display = 'none';
|
||||||
currentSearchQuery = '';
|
currentSearchQuery = '';
|
||||||
|
|
||||||
if (currentKeyFilter || currentTypeFilter) {
|
if (currentKeyFilter) {
|
||||||
loadAllData();
|
loadAllData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -396,124 +359,13 @@ function clearKeyFilter() {
|
|||||||
loadAllData();
|
loadAllData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initTypeFilter() {
|
|
||||||
if (!typeFilterSelect) return;
|
|
||||||
typeFilterSelect.innerHTML = '<option value="">全部类型</option>';
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/elastic/types-for-filter/', { credentials: 'same-origin' });
|
|
||||||
const data = await resp.json();
|
|
||||||
if (data.status !== 'success') return;
|
|
||||||
const types = data.data || [];
|
|
||||||
types.forEach(t => {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = String(t || '');
|
|
||||||
opt.textContent = String(t || '');
|
|
||||||
typeFilterSelect.appendChild(opt);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
typeFilterSelect.addEventListener('change', () => {
|
|
||||||
currentTypeFilter = (typeFilterSelect.value || '').trim();
|
|
||||||
loadAllData();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearTypeFilter() {
|
|
||||||
currentTypeFilter = '';
|
|
||||||
if (typeFilterSelect) typeFilterSelect.value = '';
|
|
||||||
loadAllData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildReportParams() {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (currentKeyFilter) params.set('key', currentKeyFilter);
|
|
||||||
if (currentTypeFilter) params.set('type', currentTypeFilter);
|
|
||||||
const iv = (reportIntervalSelect && reportIntervalSelect.value) ? reportIntervalSelect.value : 'day';
|
|
||||||
params.set('interval', iv);
|
|
||||||
const fromVal = reportFromInput ? (reportFromInput.value || '').trim() : '';
|
|
||||||
const toVal = reportToInput ? (reportToInput.value || '').trim() : '';
|
|
||||||
if (fromVal) params.set('from', fromVal);
|
|
||||||
if (toVal) params.set('to', toVal);
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateReport() {
|
|
||||||
if (!reportBox) return;
|
|
||||||
reportBox.style.display = 'block';
|
|
||||||
reportBox.className = 'search-result';
|
|
||||||
reportBox.innerHTML = '<div>正在生成报表...</div>';
|
|
||||||
try {
|
|
||||||
const params = buildReportParams();
|
|
||||||
const resp = await fetch(`/elastic/report/?${params.toString()}`, { credentials: 'same-origin' });
|
|
||||||
const ct = (resp.headers.get('content-type') || '').toLowerCase();
|
|
||||||
if (!ct.includes('application/json')) {
|
|
||||||
const text = await resp.text();
|
|
||||||
throw new Error(text ? String(text).slice(0, 200) : `HTTP ${resp.status}`);
|
|
||||||
}
|
|
||||||
const data = await resp.json();
|
|
||||||
if (data.status !== 'success') {
|
|
||||||
reportBox.className = 'search-result error';
|
|
||||||
reportBox.innerHTML = `<div>生成失败:${data.message || '未知错误'}</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const r = data.data || {};
|
|
||||||
const total = r.total || 0;
|
|
||||||
const byType = r.by_type || [];
|
|
||||||
const byTime = r.by_time || [];
|
|
||||||
const rng = r.range || {};
|
|
||||||
const flt = r.filters || {};
|
|
||||||
const lines = [];
|
|
||||||
const filterParts = [];
|
|
||||||
if (flt.key) filterParts.push(`Key:${flt.key}`);
|
|
||||||
if (flt.type) filterParts.push(`类型:${flt.type}`);
|
|
||||||
if (flt.interval) filterParts.push(`粒度:${flt.interval}`);
|
|
||||||
lines.push(`<div style="font-weight: 600;">总数:${total}</div>`);
|
|
||||||
if (rng.from || rng.to) {
|
|
||||||
lines.push(`<div class="muted" style="margin-top: 4px;">时间范围:${(rng.from || '')} ~ ${(rng.to || '')}</div>`);
|
|
||||||
}
|
|
||||||
if (filterParts.length) {
|
|
||||||
lines.push(`<div class="muted" style="margin-top: 4px;">筛查:${filterParts.join(',')}</div>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeRows = byType.map(it => `<tr><td style="padding:4px 8px;">${it.type || ''}</td><td style="padding:4px 8px; text-align:right;">${it.count || 0}</td></tr>`).join('');
|
|
||||||
const timeRows = byTime.map(it => `<tr><td style="padding:4px 8px;">${it.bucket || ''}</td><td style="padding:4px 8px; text-align:right;">${it.count || 0}</td></tr>`).join('');
|
|
||||||
|
|
||||||
lines.push(`<div style="display:flex; gap:12px; flex-wrap: wrap; margin-top: 10px;">
|
|
||||||
<div style="min-width: 260px; flex: 1;">
|
|
||||||
<div style="font-weight: 600; margin-bottom: 6px;">按成果类型</div>
|
|
||||||
<table style="width:100%; border-collapse: collapse;">
|
|
||||||
<thead><tr><th style="text-align:left; padding:4px 8px; border-bottom:1px solid #eee;">类型</th><th style="text-align:right; padding:4px 8px; border-bottom:1px solid #eee;">数量</th></tr></thead>
|
|
||||||
<tbody>${typeRows || '<tr><td colspan="2" class="muted" style="padding:6px 8px;">暂无</td></tr>'}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div style="min-width: 260px; flex: 1;">
|
|
||||||
<div style="font-weight: 600; margin-bottom: 6px;">按时间</div>
|
|
||||||
<table style="width:100%; border-collapse: collapse;">
|
|
||||||
<thead><tr><th style="text-align:left; padding:4px 8px; border-bottom:1px solid #eee;">时间</th><th style="text-align:right; padding:4px 8px; border-bottom:1px solid #eee;">数量</th></tr></thead>
|
|
||||||
<tbody>${timeRows || '<tr><td colspan="2" class="muted" style="padding:6px 8px;">暂无</td></tr>'}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>`);
|
|
||||||
|
|
||||||
reportBox.innerHTML = lines.join('');
|
|
||||||
} catch (e) {
|
|
||||||
reportBox.className = 'search-result error';
|
|
||||||
reportBox.innerHTML = `<div>生成失败:${e.message || '未知错误'}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadReportCsv() {
|
|
||||||
const params = buildReportParams();
|
|
||||||
window.location.href = `/elastic/report/csv/?${params.toString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染表格
|
// 渲染表格
|
||||||
function renderTable(data) {
|
function renderTable(data) {
|
||||||
tableBody.innerHTML = '';
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = '<td colspan="5" style="text-align: center; color: #999;">暂无数据</td>';
|
row.innerHTML = '<td colspan="4" style="text-align: center; color: #999;">暂无数据</td>';
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -570,7 +422,6 @@ function renderTable(data) {
|
|||||||
<td>
|
<td>
|
||||||
${displayData}
|
${displayData}
|
||||||
</td>
|
</td>
|
||||||
<td style="font-size: 12px; white-space: nowrap;">${formatDateTime(item.time)}</td>
|
|
||||||
<td style="font-size: 12px;">${item.writer_name || item.writer_id || ''}</td>
|
<td style="font-size: 12px;">${item.writer_name || item.writer_id || ''}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-primary" onclick="openEdit('${item._id || item.id}')">编辑</button>
|
<button class="btn btn-primary" onclick="openEdit('${item._id || item.id}')">编辑</button>
|
||||||
@@ -581,18 +432,6 @@ function renderTable(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateTime(t) {
|
|
||||||
if (!t) return '';
|
|
||||||
try {
|
|
||||||
const d = new Date(t);
|
|
||||||
if (String(d) === 'Invalid Date') return String(t);
|
|
||||||
const pad = n => String(n).padStart(2, '0');
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
||||||
} catch (e) {
|
|
||||||
return String(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildImageCell(item) {
|
function buildImageCell(item) {
|
||||||
const urls = Array.isArray(item.image_urls) ? item.image_urls : (item.image_url ? [item.image_url] : []);
|
const urls = Array.isArray(item.image_urls) ? item.image_urls : (item.image_url ? [item.image_url] : []);
|
||||||
if (!urls.length) return '无图片';
|
if (!urls.length) return '无图片';
|
||||||
@@ -828,18 +667,6 @@ async function doDelete(id){
|
|||||||
// 页面加载时自动加载所有数据
|
// 页面加载时自动加载所有数据
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
initKeyFilter();
|
initKeyFilter();
|
||||||
initTypeFilter();
|
|
||||||
if (reportFromInput && reportToInput && reportIntervalSelect) {
|
|
||||||
const now = new Date();
|
|
||||||
const pad = n => String(n).padStart(2, '0');
|
|
||||||
const toLocal = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
||||||
if (!reportToInput.value) reportToInput.value = toLocal(now);
|
|
||||||
if (!reportFromInput.value) {
|
|
||||||
const from = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
||||||
reportFromInput.value = toLocal(from);
|
|
||||||
}
|
|
||||||
if (!reportIntervalSelect.value) reportIntervalSelect.value = 'day';
|
|
||||||
}
|
|
||||||
loadAllData();
|
loadAllData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -494,11 +494,6 @@ uploadForm.addEventListener('submit', async (e) => {
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
const ct = (resp.headers.get('content-type') || '').toLowerCase();
|
|
||||||
if (!ct.includes('application/json')) {
|
|
||||||
const text = await resp.text();
|
|
||||||
throw new Error(text ? String(text).slice(0, 200) : `HTTP ${resp.status}`);
|
|
||||||
}
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok || data.status !== 'success') {
|
if (!resp.ok || data.status !== 'success') {
|
||||||
throw new Error(data.message || '上传识别失败');
|
throw new Error(data.message || '上传识别失败');
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ urlpatterns = [
|
|||||||
path('all-data/', views.get_all_data, name='get_all_data'),
|
path('all-data/', views.get_all_data, name='get_all_data'),
|
||||||
path('filter-by-key/', views.filter_by_key, name='filter_by_key'),
|
path('filter-by-key/', views.filter_by_key, name='filter_by_key'),
|
||||||
path('keys-for-filter/', views.keys_for_filter_view, name='keys_for_filter'),
|
path('keys-for-filter/', views.keys_for_filter_view, name='keys_for_filter'),
|
||||||
path('types-for-filter/', views.types_for_filter_view, name='types_for_filter'),
|
|
||||||
path('filter/', views.filter_view, name='filter'),
|
|
||||||
path('report/', views.report_view, name='report'),
|
|
||||||
path('report/csv/', views.report_csv_view, name='report_csv'),
|
|
||||||
|
|
||||||
# 用户管理
|
# 用户管理
|
||||||
path('users/', views.get_users, name='get_users'),
|
path('users/', views.get_users, name='get_users'),
|
||||||
|
|||||||
497
elastic/views.py
497
elastic/views.py
@@ -6,13 +6,10 @@ import re
|
|||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import csv
|
|
||||||
import io
|
|
||||||
from datetime import datetime, timezone, timedelta
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import JsonResponse, HttpResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
@@ -722,117 +719,115 @@ def upload_page(request):
|
|||||||
# 上传并识别(不入库)
|
# 上传并识别(不入库)
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
def upload(request):
|
def upload(request):
|
||||||
try:
|
if request.session.get("user_id") is None:
|
||||||
if request.session.get("user_id") is None:
|
fallback_uid = request.POST.get("user_id") or request.GET.get("user_id")
|
||||||
fallback_uid = request.POST.get("user_id") or request.GET.get("user_id")
|
if fallback_uid:
|
||||||
if fallback_uid:
|
request.session["user_id"] = fallback_uid
|
||||||
request.session["user_id"] = fallback_uid
|
request.session.setdefault("permission", 1)
|
||||||
request.session.setdefault("permission", 1)
|
else:
|
||||||
else:
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
|
||||||
|
|
||||||
files = request.FILES.getlist("file")
|
files = request.FILES.getlist("file")
|
||||||
if not files:
|
if not files:
|
||||||
one = request.FILES.get("file")
|
one = request.FILES.get("file")
|
||||||
if one:
|
if one:
|
||||||
files = [one]
|
files = [one]
|
||||||
if not files:
|
if not files:
|
||||||
return JsonResponse({"status": "error", "message": "未选择文件"}, status=400)
|
return JsonResponse({"status": "error", "message": "未选择文件"}, status=400)
|
||||||
|
|
||||||
images_dir = os.path.join(settings.MEDIA_ROOT, "images")
|
images_dir = os.path.join(settings.MEDIA_ROOT, "images")
|
||||||
os.makedirs(images_dir, exist_ok=True)
|
os.makedirs(images_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 按照原始文件进行分组处理
|
||||||
|
file_results = []
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
group_images = [] # 存储该文件生成的所有图片路径信息 (abs_path, filename)
|
||||||
|
is_pdf = f.name.lower().endswith('.pdf')
|
||||||
|
|
||||||
file_results = []
|
if is_pdf:
|
||||||
|
if not HAS_PDF_SUPPORT:
|
||||||
for f in files:
|
return JsonResponse({"status": "error", "message": f"服务器未安装PDF处理组件(PyMuPDF): {PDF_ERROR}"}, status=500)
|
||||||
group_images = []
|
|
||||||
is_pdf = f.name.lower().endswith('.pdf')
|
|
||||||
|
|
||||||
if is_pdf:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
|
||||||
if not HAS_PDF_SUPPORT:
|
for chunk in f.chunks():
|
||||||
return JsonResponse({"status": "error", "message": f"服务器未安装PDF处理组件(PyMuPDF): {PDF_ERROR}"}, status=500)
|
tmp.write(chunk)
|
||||||
|
tmp_path = tmp.name
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
|
|
||||||
for chunk in f.chunks():
|
try:
|
||||||
tmp.write(chunk)
|
doc = fitz.open(tmp_path)
|
||||||
tmp_path = tmp.name
|
for i in range(len(doc)):
|
||||||
|
page = doc.load_page(i)
|
||||||
try:
|
pix = page.get_pixmap(dpi=150)
|
||||||
doc = fitz.open(tmp_path)
|
img_filename = f"{uuid.uuid4()}_page_{i+1}.jpg"
|
||||||
for i in range(len(doc)):
|
img_abs_path = os.path.join(images_dir, img_filename)
|
||||||
page = doc.load_page(i)
|
pix.save(img_abs_path)
|
||||||
pix = page.get_pixmap(dpi=150)
|
group_images.append((img_abs_path, img_filename))
|
||||||
img_filename = f"{uuid.uuid4()}_page_{i+1}.jpg"
|
doc.close()
|
||||||
img_abs_path = os.path.join(images_dir, img_filename)
|
except Exception as e:
|
||||||
pix.save(img_abs_path)
|
return JsonResponse({"status": "error", "message": f"PDF {f.name} 转换失败: {str(e)}"}, status=500)
|
||||||
group_images.append((img_abs_path, img_filename))
|
finally:
|
||||||
doc.close()
|
if os.path.exists(tmp_path):
|
||||||
except Exception as e:
|
os.remove(tmp_path)
|
||||||
return JsonResponse({"status": "error", "message": f"PDF {f.name} 转换失败: {str(e)}"}, status=500)
|
else:
|
||||||
finally:
|
filename = f"{uuid.uuid4()}_{f.name}"
|
||||||
if os.path.exists(tmp_path):
|
abs_path = os.path.join(images_dir, filename)
|
||||||
os.remove(tmp_path)
|
with open(abs_path, "wb") as dst:
|
||||||
else:
|
for chunk in f.chunks():
|
||||||
filename = f"{uuid.uuid4()}_{f.name}"
|
dst.write(chunk)
|
||||||
abs_path = os.path.join(images_dir, filename)
|
group_images.append((abs_path, filename))
|
||||||
with open(abs_path, "wb") as dst:
|
|
||||||
for chunk in f.chunks():
|
|
||||||
dst.write(chunk)
|
|
||||||
group_images.append((abs_path, filename))
|
|
||||||
|
|
||||||
def run_ocr(img_info):
|
# 对该组图片并行进行 OCR 识别
|
||||||
abs_p, fname = img_info
|
def run_ocr(img_info):
|
||||||
try:
|
abs_p, fname = img_info
|
||||||
data = ocr_and_extract_info(abs_p)
|
try:
|
||||||
return data
|
data = ocr_and_extract_info(abs_p)
|
||||||
except Exception:
|
return data
|
||||||
return None
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
group_data_list = []
|
group_data_list = []
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(group_images), 8)) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(group_images), 8)) as executor:
|
||||||
futures = [executor.submit(run_ocr, img_info) for img_info in group_images]
|
futures = [executor.submit(run_ocr, img_info) for img_info in group_images]
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
res = future.result()
|
res = future.result()
|
||||||
if res:
|
if res:
|
||||||
group_data_list.append(res)
|
group_data_list.append(res)
|
||||||
|
|
||||||
merged_group_data = {}
|
# 合并该文件的多页识别结果
|
||||||
for item in group_data_list:
|
merged_group_data = {}
|
||||||
if not isinstance(item, dict):
|
for item in group_data_list:
|
||||||
continue
|
if not isinstance(item, dict): continue
|
||||||
for k, v in item.items():
|
for k, v in item.items():
|
||||||
key = str(k).strip()
|
key = str(k).strip()
|
||||||
if not key:
|
if not key: continue
|
||||||
continue
|
if key not in merged_group_data or merged_group_data.get(key) in (None, ''):
|
||||||
if key not in merged_group_data or merged_group_data.get(key) in (None, ''):
|
merged_group_data[key] = v
|
||||||
merged_group_data[key] = v
|
elif merged_group_data.get(key) != v:
|
||||||
elif merged_group_data.get(key) != v:
|
base = key
|
||||||
base = key
|
idx = 2
|
||||||
idx = 2
|
while f"{base}_{idx}" in merged_group_data: idx += 1
|
||||||
while f"{base}_{idx}" in merged_group_data:
|
merged_group_data[f"{base}_{idx}"] = v
|
||||||
idx += 1
|
|
||||||
merged_group_data[f"{base}_{idx}"] = v
|
|
||||||
|
|
||||||
if not merged_group_data:
|
if not merged_group_data:
|
||||||
merged_group_data = {"文件名": f.name, "提示": "未识别到具体内容"}
|
# 如果没识别到,至少保留一个空结构或者包含文件名的提示
|
||||||
|
merged_group_data = {"文件名": f.name, "提示": "未识别到具体内容"}
|
||||||
|
|
||||||
rel_paths = [f"images/{img[1]}" for img in group_images]
|
rel_paths = [f"images/{img[1]}" for img in group_images]
|
||||||
image_urls = [request.build_absolute_uri(settings.MEDIA_URL + rp) for rp in rel_paths]
|
image_urls = [request.build_absolute_uri(settings.MEDIA_URL + rp) for rp in rel_paths]
|
||||||
|
|
||||||
file_results.append({
|
file_results.append({
|
||||||
"name": f.name,
|
"name": f.name,
|
||||||
"data": merged_group_data,
|
"data": merged_group_data,
|
||||||
"images": rel_paths,
|
"images": rel_paths,
|
||||||
"image_urls": image_urls,
|
"image_urls": image_urls,
|
||||||
})
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
"status": "success",
|
|
||||||
"message": f"成功处理 {len(file_results)} 个文件,请确认数据后点击录入",
|
|
||||||
"items": file_results,
|
|
||||||
})
|
})
|
||||||
except Exception as e:
|
|
||||||
return JsonResponse({"status": "error", "message": str(e) or "上传失败"}, status=500)
|
return JsonResponse({
|
||||||
|
"status": "success",
|
||||||
|
"message": f"成功处理 {len(file_results)} 个文件,请确认数据后点击录入",
|
||||||
|
"items": file_results,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# 确认并入库
|
# 确认并入库
|
||||||
@@ -958,7 +953,6 @@ def manage_page(request):
|
|||||||
|
|
||||||
me = get_user_by_id(session_user_id) or {}
|
me = get_user_by_id(session_user_id) or {}
|
||||||
is_admin = int(request.session.get("permission", 1)) == 0
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
has_manage_key = bool(me.get("manage_key") or [])
|
|
||||||
if is_admin:
|
if is_admin:
|
||||||
raw_results = search_all()
|
raw_results = search_all()
|
||||||
else:
|
else:
|
||||||
@@ -986,7 +980,6 @@ def manage_page(request):
|
|||||||
"elastic/manage.html",
|
"elastic/manage.html",
|
||||||
{
|
{
|
||||||
"is_admin": is_admin,
|
"is_admin": is_admin,
|
||||||
"has_manage_key": has_manage_key,
|
|
||||||
"user_id": session_user_id,
|
"user_id": session_user_id,
|
||||||
"username": me.get("username"),
|
"username": me.get("username"),
|
||||||
},
|
},
|
||||||
@@ -1027,7 +1020,7 @@ def analytics_types_view(request):
|
|||||||
size_int = int(size) if size is not None else 10
|
size_int = int(size) if size is not None else 10
|
||||||
except Exception:
|
except Exception:
|
||||||
size_int = 10
|
size_int = 10
|
||||||
data = es_analytics_types(gte=gte, lte=lte, limit=size_int)
|
data = es_analytics_types(gte=gte, lte=lte, size=size_int)
|
||||||
return JsonResponse({"status": "success", "data": data})
|
return JsonResponse({"status": "success", "data": data})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||||
@@ -1435,296 +1428,6 @@ def keys_for_filter_view(request):
|
|||||||
add(out, seen, v)
|
add(out, seen, v)
|
||||||
return JsonResponse({"status": "success", "data": out})
|
return JsonResponse({"status": "success", "data": out})
|
||||||
|
|
||||||
|
|
||||||
def _extract_type_from_data(value):
|
|
||||||
s = value
|
|
||||||
if s is None:
|
|
||||||
return ""
|
|
||||||
if not isinstance(s, str):
|
|
||||||
try:
|
|
||||||
s = json.dumps(s, ensure_ascii=False)
|
|
||||||
except Exception:
|
|
||||||
s = str(s)
|
|
||||||
s = str(s)
|
|
||||||
try:
|
|
||||||
obj = json.loads(s)
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
t = obj.get("数据类型")
|
|
||||||
if isinstance(t, str) and t.strip():
|
|
||||||
return t.strip()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
m = re.search(r'"数据类型"\s*:\s*"([^"]+)"', s)
|
|
||||||
if m:
|
|
||||||
return str(m.group(1)).strip()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
|
||||||
def types_for_filter_view(request):
|
|
||||||
uid = request.session.get("user_id")
|
|
||||||
if uid is None:
|
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
|
||||||
try:
|
|
||||||
types = [str(t).strip() for t in (get_type_list() or []) if str(t).strip()]
|
|
||||||
except Exception:
|
|
||||||
types = []
|
|
||||||
seen = set()
|
|
||||||
out = []
|
|
||||||
for t in types:
|
|
||||||
if t in seen:
|
|
||||||
continue
|
|
||||||
seen.add(t)
|
|
||||||
out.append(t)
|
|
||||||
return JsonResponse({"status": "success", "data": out})
|
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
|
||||||
def filter_view(request):
|
|
||||||
session_user_id = request.session.get("user_id")
|
|
||||||
if session_user_id is None:
|
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
|
||||||
|
|
||||||
key = (request.GET.get("key") or "").strip()
|
|
||||||
typ = (request.GET.get("type") or "").strip()
|
|
||||||
|
|
||||||
results = search_all()
|
|
||||||
results = _filter_results_for_user(request, results)
|
|
||||||
|
|
||||||
filtered = list(results or [])
|
|
||||||
|
|
||||||
if key:
|
|
||||||
selected = str(key).strip()
|
|
||||||
try:
|
|
||||||
users = get_all_users() or []
|
|
||||||
except Exception:
|
|
||||||
users = []
|
|
||||||
writer_keys_by_id = {}
|
|
||||||
for u in users:
|
|
||||||
try:
|
|
||||||
u_id = str(u.get("user_id", "")).strip()
|
|
||||||
except Exception:
|
|
||||||
u_id = ""
|
|
||||||
if not u_id:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
u_keys = {str(k).strip() for k in (u.get("key") or []) if str(k).strip()}
|
|
||||||
except Exception:
|
|
||||||
u_keys = set()
|
|
||||||
writer_keys_by_id[u_id] = u_keys
|
|
||||||
|
|
||||||
tmp = []
|
|
||||||
for r in filtered:
|
|
||||||
writer_id = str(r.get("writer_id", "")).strip()
|
|
||||||
writer_keys = writer_keys_by_id.get(writer_id)
|
|
||||||
if writer_keys and selected in writer_keys:
|
|
||||||
tmp.append(r)
|
|
||||||
continue
|
|
||||||
if selected and selected in str(r.get("data", "")):
|
|
||||||
tmp.append(r)
|
|
||||||
filtered = tmp
|
|
||||||
|
|
||||||
if typ:
|
|
||||||
tsel = str(typ).strip()
|
|
||||||
filtered = [r for r in filtered if _extract_type_from_data(r.get("data")) == tsel]
|
|
||||||
|
|
||||||
data = _attach_writer_names(_attach_image_urls(request, filtered))
|
|
||||||
return JsonResponse({"status": "success", "data": data})
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_dt(value):
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
if hasattr(value, "isoformat"):
|
|
||||||
try:
|
|
||||||
dt = value
|
|
||||||
if getattr(dt, "tzinfo", None) is None:
|
|
||||||
dt = dt.replace(tzinfo=timezone.utc)
|
|
||||||
return dt
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
s = str(value).strip()
|
|
||||||
if not s:
|
|
||||||
return None
|
|
||||||
if s.endswith("Z"):
|
|
||||||
s = s[:-1] + "+00:00"
|
|
||||||
try:
|
|
||||||
dt = datetime.fromisoformat(s)
|
|
||||||
if getattr(dt, "tzinfo", None) is None:
|
|
||||||
dt = dt.replace(tzinfo=timezone.utc)
|
|
||||||
return dt
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _floor_dt(dt, interval: str):
|
|
||||||
if not dt:
|
|
||||||
return None
|
|
||||||
iv = str(interval or "day").strip().lower()
|
|
||||||
if iv == "month":
|
|
||||||
return dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
if iv == "week":
|
|
||||||
base = dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
return base - timedelta(days=base.weekday())
|
|
||||||
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
|
|
||||||
|
|
||||||
def _dt_label(dt, interval: str):
|
|
||||||
if not dt:
|
|
||||||
return ""
|
|
||||||
iv = str(interval or "day").strip().lower()
|
|
||||||
if iv == "month":
|
|
||||||
return dt.strftime("%Y-%m")
|
|
||||||
return dt.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
|
||||||
def report_view(request):
|
|
||||||
try:
|
|
||||||
session_user_id = request.session.get("user_id")
|
|
||||||
if session_user_id is None:
|
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
|
||||||
is_admin = int(request.session.get("permission", 1)) == 0
|
|
||||||
me = get_user_by_id(session_user_id) or {}
|
|
||||||
has_manage_key = bool(me.get("manage_key") or [])
|
|
||||||
if (not is_admin) and (not has_manage_key):
|
|
||||||
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
|
||||||
|
|
||||||
gte = (request.GET.get("from") or "").strip()
|
|
||||||
lte = (request.GET.get("to") or "").strip()
|
|
||||||
interval = (request.GET.get("interval") or "day").strip()
|
|
||||||
key = (request.GET.get("key") or "").strip()
|
|
||||||
typ = (request.GET.get("type") or "").strip()
|
|
||||||
|
|
||||||
gte_dt = _parse_dt(gte) if gte else None
|
|
||||||
lte_dt = _parse_dt(lte) if lte else None
|
|
||||||
|
|
||||||
results = search_all()
|
|
||||||
results = _filter_results_for_user(request, results)
|
|
||||||
filtered = list(results or [])
|
|
||||||
|
|
||||||
if key:
|
|
||||||
selected = str(key).strip()
|
|
||||||
try:
|
|
||||||
users = get_all_users() or []
|
|
||||||
except Exception:
|
|
||||||
users = []
|
|
||||||
writer_keys_by_id = {}
|
|
||||||
for u in users:
|
|
||||||
try:
|
|
||||||
u_id = str(u.get("user_id", "")).strip()
|
|
||||||
except Exception:
|
|
||||||
u_id = ""
|
|
||||||
if not u_id:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
u_keys = {str(k).strip() for k in (u.get("key") or []) if str(k).strip()}
|
|
||||||
except Exception:
|
|
||||||
u_keys = set()
|
|
||||||
writer_keys_by_id[u_id] = u_keys
|
|
||||||
|
|
||||||
tmp = []
|
|
||||||
for r in filtered:
|
|
||||||
writer_id = str(r.get("writer_id", "")).strip()
|
|
||||||
writer_keys = writer_keys_by_id.get(writer_id)
|
|
||||||
if writer_keys and selected in writer_keys:
|
|
||||||
tmp.append(r)
|
|
||||||
continue
|
|
||||||
if selected and selected in str(r.get("data", "")):
|
|
||||||
tmp.append(r)
|
|
||||||
filtered = tmp
|
|
||||||
|
|
||||||
if typ:
|
|
||||||
tsel = str(typ).strip()
|
|
||||||
filtered = [r for r in filtered if _extract_type_from_data(r.get("data")) == tsel]
|
|
||||||
|
|
||||||
ranged = []
|
|
||||||
for r in filtered:
|
|
||||||
t = _parse_dt(r.get("time"))
|
|
||||||
if (gte_dt or lte_dt) and (t is None):
|
|
||||||
continue
|
|
||||||
if gte_dt and t and t < gte_dt:
|
|
||||||
continue
|
|
||||||
if lte_dt and t and t > lte_dt:
|
|
||||||
continue
|
|
||||||
rr = dict(r)
|
|
||||||
rr["_time_dt"] = t
|
|
||||||
ranged.append(rr)
|
|
||||||
|
|
||||||
by_type = {}
|
|
||||||
by_bucket = {}
|
|
||||||
for r in ranged:
|
|
||||||
tval = _extract_type_from_data(r.get("data"))
|
|
||||||
if tval:
|
|
||||||
by_type[tval] = by_type.get(tval, 0) + 1
|
|
||||||
bucket_dt = _floor_dt(r.get("_time_dt"), interval)
|
|
||||||
if bucket_dt:
|
|
||||||
label = _dt_label(bucket_dt, interval)
|
|
||||||
if label:
|
|
||||||
by_bucket[label] = by_bucket.get(label, 0) + 1
|
|
||||||
|
|
||||||
by_type_arr = [{"type": k, "count": int(v)} for k, v in sorted(by_type.items(), key=lambda x: (-x[1], x[0]))]
|
|
||||||
by_time_arr = [{"bucket": k, "count": int(v)} for k, v in sorted(by_bucket.items(), key=lambda x: x[0])]
|
|
||||||
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
||||||
"range": {"from": gte or "", "to": lte or ""},
|
|
||||||
"filters": {"key": key or "", "type": typ or "", "interval": interval or "day"},
|
|
||||||
"total": len(ranged),
|
|
||||||
"by_type": by_type_arr,
|
|
||||||
"by_time": by_time_arr,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return JsonResponse({"status": "error", "message": str(e) or "生成失败"}, status=500)
|
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
|
||||||
def report_csv_view(request):
|
|
||||||
resp = report_view(request)
|
|
||||||
if getattr(resp, "status_code", 200) != 200:
|
|
||||||
return resp
|
|
||||||
try:
|
|
||||||
payload = json.loads(resp.content.decode("utf-8"))
|
|
||||||
except Exception:
|
|
||||||
return JsonResponse({"status": "error", "message": "生成失败"}, status=500)
|
|
||||||
data = (payload or {}).get("data") or {}
|
|
||||||
|
|
||||||
buf = io.StringIO()
|
|
||||||
w = csv.writer(buf)
|
|
||||||
w.writerow(["统计报表"])
|
|
||||||
w.writerow(["生成时间", data.get("generated_at", "")])
|
|
||||||
rng = data.get("range") or {}
|
|
||||||
w.writerow(["时间范围", f"{rng.get('from','')} ~ {rng.get('to','')}"])
|
|
||||||
flt = data.get("filters") or {}
|
|
||||||
w.writerow(["筛查Key", flt.get("key", "")])
|
|
||||||
w.writerow(["筛查类型", flt.get("type", "")])
|
|
||||||
w.writerow(["时间粒度", flt.get("interval", "")])
|
|
||||||
w.writerow([])
|
|
||||||
w.writerow(["总数", data.get("total", 0)])
|
|
||||||
w.writerow([])
|
|
||||||
w.writerow(["按成果类型统计"])
|
|
||||||
w.writerow(["类型", "数量"])
|
|
||||||
for it in list(data.get("by_type") or []):
|
|
||||||
w.writerow([it.get("type", ""), it.get("count", 0)])
|
|
||||||
w.writerow([])
|
|
||||||
w.writerow(["按时间统计"])
|
|
||||||
w.writerow(["时间", "数量"])
|
|
||||||
for it in list(data.get("by_time") or []):
|
|
||||||
w.writerow([it.get("bucket", ""), it.get("count", 0)])
|
|
||||||
|
|
||||||
content = buf.getvalue()
|
|
||||||
out = HttpResponse(content, content_type="text/csv; charset=utf-8")
|
|
||||||
out["Content-Disposition"] = 'attachment; filename="report.csv"'
|
|
||||||
return out
|
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@csrf_protect
|
@csrf_protect
|
||||||
def revoke_registration_code_view(request):
|
def revoke_registration_code_view(request):
|
||||||
|
|||||||
Reference in New Issue
Block a user