打包项目
This commit is contained in:
12
frontend-vue/convert.html
Normal file
12
frontend-vue/convert.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>格式转换 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/convert-main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
frontend-vue/dedupe.html
Normal file
12
frontend-vue/dedupe.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>数据去重 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/dedupe-main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
frontend-vue/split.html
Normal file
12
frontend-vue/split.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>数据拆分 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/split-main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view />
|
|
||||||
</template>
|
|
||||||
7
frontend-vue/src/convert-main.ts
Normal file
7
frontend-vue/src/convert-main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import '@/styles/main.css'
|
||||||
|
import BrandConvertTab from '@/pages/brand/components/BrandConvertTab.vue'
|
||||||
|
|
||||||
|
createApp(BrandConvertTab).use(ElementPlus).mount('#app')
|
||||||
7
frontend-vue/src/dedupe-main.ts
Normal file
7
frontend-vue/src/dedupe-main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import '@/styles/main.css'
|
||||||
|
import BrandDedupeTab from '@/pages/brand/components/BrandDedupeTab.vue'
|
||||||
|
|
||||||
|
createApp(BrandDedupeTab).use(ElementPlus).mount('#app')
|
||||||
@@ -1,584 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="light">
|
|
||||||
<div class="admin-page">
|
|
||||||
<div class="admin-nav">
|
|
||||||
<router-link to="/home">← 返回首页</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="page-title">管理后台</h1>
|
|
||||||
|
|
||||||
<el-tabs v-model="activeTab" class="admin-tabs" @tab-change="handleTabChange">
|
|
||||||
<el-tab-pane label="用户管理" name="users">
|
|
||||||
<div class="admin-grid">
|
|
||||||
<el-card class="form-card">
|
|
||||||
<template #header>创建用户</template>
|
|
||||||
|
|
||||||
<p class="card-tip">无注册入口,仅管理员可在此创建用户。层级:超级管理员 → 管理员 → 普通号。</p>
|
|
||||||
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item label="用户名">
|
|
||||||
<el-input v-model="createForm.username" placeholder="用户名(至少2个字符)" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="密码">
|
|
||||||
<el-input
|
|
||||||
v-model="createForm.password"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="密码(至少6个字符)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="showRoleSelect" label="角色">
|
|
||||||
<el-select v-model="createForm.role">
|
|
||||||
<el-option label="普通号" value="normal" />
|
|
||||||
<el-option label="管理员" value="admin" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="showCreatedBySelect" label="所属管理员">
|
|
||||||
<el-select v-model="createForm.createdById" placeholder="请选择管理员">
|
|
||||||
<el-option
|
|
||||||
v-for="admin in adminsList"
|
|
||||||
:key="admin.id"
|
|
||||||
:label="admin.username"
|
|
||||||
:value="admin.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-button type="primary" :loading="createLoading" @click="handleCreateUser">创建用户</el-button>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="table-card">
|
|
||||||
<template #header>用户列表</template>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-input
|
|
||||||
v-model="searchUsername"
|
|
||||||
placeholder="输入用户名关键字"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="loadUsers(1)"
|
|
||||||
/>
|
|
||||||
<el-select
|
|
||||||
v-if="isSuperAdmin"
|
|
||||||
v-model="filterCreatedBy"
|
|
||||||
placeholder="所属管理员"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="admin in adminsList"
|
|
||||||
:key="admin.id"
|
|
||||||
:label="admin.username"
|
|
||||||
:value="String(admin.id)"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-button type="primary" @click="loadUsers(1)">查询</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="users" border>
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
|
||||||
<el-table-column label="角色" width="120">
|
|
||||||
<template #default="{ row }">{{ roleLabel(row.role) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="creator_username" label="所属管理员" min-width="140">
|
|
||||||
<template #default="{ row }">{{ row.creator_username || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="row-actions">
|
|
||||||
<el-button size="small" @click="openEditDialog(row)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" plain @click="removeUser(row)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-pagination
|
|
||||||
class="pagination"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
:current-page="userPage"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:total="userTotal"
|
|
||||||
@current-change="loadUsers"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="查看生成记录" name="history">
|
|
||||||
<el-card class="table-card">
|
|
||||||
<template #header>筛选条件</template>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-select v-model="filterUserId" placeholder="全部用户" clearable>
|
|
||||||
<el-option
|
|
||||||
v-for="option in userOptions"
|
|
||||||
:key="option.id"
|
|
||||||
:label="`${option.username} (${roleLabel(option.role)})`"
|
|
||||||
:value="String(option.id)"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-date-picker
|
|
||||||
v-model="filterTimeStart"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="开始时间"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
<el-date-picker
|
|
||||||
v-model="filterTimeEnd"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="结束时间"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
<el-button type="primary" @click="loadHistory(1)">查询</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="table-card history-card">
|
|
||||||
<template #header>生成记录</template>
|
|
||||||
|
|
||||||
<el-table :data="historyRows" border>
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="username" label="用户" min-width="120">
|
|
||||||
<template #default="{ row }">{{ row.username || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="panel_type" label="类型" min-width="140">
|
|
||||||
<template #default="{ row }">{{ row.panel_type || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
|
||||||
<el-table-column label="结果预览" min-width="220">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div v-if="getHistoryPreviewUrls(row).length" class="history-preview">
|
|
||||||
<el-image
|
|
||||||
v-for="(url, index) in getHistoryPreviewUrls(row)"
|
|
||||||
:key="`${row.id}-${index}`"
|
|
||||||
:src="url"
|
|
||||||
fit="cover"
|
|
||||||
:preview-src-list="getHistoryPreviewUrls(row)"
|
|
||||||
class="preview-image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-pagination
|
|
||||||
class="pagination"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
:current-page="historyPage"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:total="historyTotal"
|
|
||||||
@current-change="loadHistory"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
|
|
||||||
<el-dialog v-model="editVisible" title="编辑用户" width="420px">
|
|
||||||
<el-form label-position="top">
|
|
||||||
<el-form-item label="用户名">
|
|
||||||
<el-input :model-value="editForm.username" readonly />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="新密码(不修改留空)">
|
|
||||||
<el-input
|
|
||||||
v-model="editForm.password"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="留空则不修改密码"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="showEditRoleSelect" label="角色">
|
|
||||||
<el-select v-model="editForm.role">
|
|
||||||
<el-option label="普通号" value="normal" />
|
|
||||||
<el-option label="管理员" value="admin" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="editForm.creatorUsername" label="所属管理员">
|
|
||||||
<el-input :model-value="editForm.creatorUsername" readonly />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="editVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="editLoading" @click="saveUser">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import {
|
|
||||||
createAdminUser,
|
|
||||||
deleteAdminUser,
|
|
||||||
getAdminHistory,
|
|
||||||
getAdminUsers,
|
|
||||||
updateAdminUser,
|
|
||||||
type AdminHistoryItem,
|
|
||||||
type AdminUserItem,
|
|
||||||
} from '@/shared/api/admin'
|
|
||||||
|
|
||||||
interface UserOption extends AdminUserItem {}
|
|
||||||
|
|
||||||
const activeTab = ref<'users' | 'history'>('users')
|
|
||||||
const pageSize = 15
|
|
||||||
|
|
||||||
const currentUserRole = ref<'super_admin' | 'admin' | 'normal' | string>('admin')
|
|
||||||
const adminsList = ref<{ id: number; username: string }[]>([])
|
|
||||||
const users = ref<AdminUserItem[]>([])
|
|
||||||
const userTotal = ref(0)
|
|
||||||
const userPage = ref(1)
|
|
||||||
const searchUsername = ref('')
|
|
||||||
const filterCreatedBy = ref('')
|
|
||||||
const createLoading = ref(false)
|
|
||||||
|
|
||||||
const createForm = reactive({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
role: 'normal',
|
|
||||||
createdById: undefined as number | undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const userOptions = ref<UserOption[]>([])
|
|
||||||
const filterUserId = ref('')
|
|
||||||
const filterTimeStart = ref('')
|
|
||||||
const filterTimeEnd = ref('')
|
|
||||||
const historyRows = ref<AdminHistoryItem[]>([])
|
|
||||||
const historyTotal = ref(0)
|
|
||||||
const historyPage = ref(1)
|
|
||||||
|
|
||||||
const editVisible = ref(false)
|
|
||||||
const editLoading = ref(false)
|
|
||||||
const editForm = reactive({
|
|
||||||
id: 0,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
role: 'normal',
|
|
||||||
creatorUsername: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const isSuperAdmin = computed(() => currentUserRole.value === 'super_admin')
|
|
||||||
const showRoleSelect = computed(() => isSuperAdmin.value)
|
|
||||||
const showCreatedBySelect = computed(
|
|
||||||
() => isSuperAdmin.value && createForm.role === 'normal' && adminsList.value.length > 0,
|
|
||||||
)
|
|
||||||
const showEditRoleSelect = computed(() => isSuperAdmin.value)
|
|
||||||
|
|
||||||
function roleLabel(role?: string) {
|
|
||||||
if (role === 'super_admin') return '超级管理员'
|
|
||||||
if (role === 'admin') return '管理员'
|
|
||||||
return '普通号'
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildUsersQuery(page: number) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
page_size: String(pageSize),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (searchUsername.value.trim()) {
|
|
||||||
params.set('username', searchUsername.value.trim())
|
|
||||||
}
|
|
||||||
if (filterCreatedBy.value) {
|
|
||||||
params.set('created_by_id', filterCreatedBy.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUsers(page = 1) {
|
|
||||||
userPage.value = page
|
|
||||||
try {
|
|
||||||
const result = await getAdminUsers(buildUsersQuery(page))
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '加载用户失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUserRole.value = result.current_user_role || 'admin'
|
|
||||||
adminsList.value = result.admins || []
|
|
||||||
users.value = result.items || []
|
|
||||||
userTotal.value = result.total || 0
|
|
||||||
|
|
||||||
if (!isSuperAdmin.value) {
|
|
||||||
createForm.role = 'normal'
|
|
||||||
createForm.createdById = undefined
|
|
||||||
filterCreatedBy.value = ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUserOptions() {
|
|
||||||
try {
|
|
||||||
const result = await getAdminUsers('page=1&page_size=999')
|
|
||||||
if (result.success) {
|
|
||||||
userOptions.value = result.items || []
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
userOptions.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHistoryQuery(page: number) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
page_size: String(pageSize),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (filterUserId.value) {
|
|
||||||
params.set('user_id', filterUserId.value)
|
|
||||||
}
|
|
||||||
if (filterTimeStart.value) {
|
|
||||||
params.set('time_start', filterTimeStart.value)
|
|
||||||
}
|
|
||||||
if (filterTimeEnd.value) {
|
|
||||||
params.set('time_end', filterTimeEnd.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadHistory(page = 1) {
|
|
||||||
historyPage.value = page
|
|
||||||
try {
|
|
||||||
const result = await getAdminHistory(buildHistoryQuery(page))
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '加载记录失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
historyRows.value = result.items || []
|
|
||||||
historyTotal.value = result.total || 0
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCreateUser() {
|
|
||||||
const username = createForm.username.trim()
|
|
||||||
if (username.length < 2) {
|
|
||||||
ElMessage.warning('用户名至少2个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createForm.password.length < 6) {
|
|
||||||
ElMessage.warning('密码至少6个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showCreatedBySelect.value && !createForm.createdById) {
|
|
||||||
ElMessage.warning('请选择所属管理员')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createLoading.value = true
|
|
||||||
try {
|
|
||||||
const payload: {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
role: string
|
|
||||||
created_by_id?: number
|
|
||||||
} = {
|
|
||||||
username,
|
|
||||||
password: createForm.password,
|
|
||||||
role: isSuperAdmin.value ? createForm.role : 'normal',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showCreatedBySelect.value && createForm.createdById) {
|
|
||||||
payload.created_by_id = createForm.createdById
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await createAdminUser(payload)
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '创建失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success(result.msg || '创建成功')
|
|
||||||
createForm.username = ''
|
|
||||||
createForm.password = ''
|
|
||||||
createForm.role = 'normal'
|
|
||||||
createForm.createdById = undefined
|
|
||||||
await loadUsers(1)
|
|
||||||
await loadUserOptions()
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
} finally {
|
|
||||||
createLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditDialog(user: AdminUserItem) {
|
|
||||||
editForm.id = user.id
|
|
||||||
editForm.username = user.username || ''
|
|
||||||
editForm.password = ''
|
|
||||||
editForm.role = user.role === 'admin' ? 'admin' : 'normal'
|
|
||||||
editForm.creatorUsername = user.creator_username || ''
|
|
||||||
editVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUser() {
|
|
||||||
editLoading.value = true
|
|
||||||
try {
|
|
||||||
const payload: { password?: string; role?: string } = {}
|
|
||||||
if (editForm.password) {
|
|
||||||
if (editForm.password.length < 6) {
|
|
||||||
ElMessage.warning('密码至少6个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payload.password = editForm.password
|
|
||||||
}
|
|
||||||
if (showEditRoleSelect.value) {
|
|
||||||
payload.role = editForm.role
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await updateAdminUser(editForm.id, payload)
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '保存失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
editVisible.value = false
|
|
||||||
ElMessage.success(result.msg || '保存成功')
|
|
||||||
await loadUsers(userPage.value)
|
|
||||||
await loadUserOptions()
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
} finally {
|
|
||||||
editLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeUser(user: AdminUserItem) {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(`确定删除用户 "${user.username || ''}" 吗?`, '删除确认', {
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await deleteAdminUser(user.id)
|
|
||||||
if (!result.success) {
|
|
||||||
ElMessage.error(result.error || '删除失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success('删除成功')
|
|
||||||
await loadUsers(userPage.value)
|
|
||||||
await loadUserOptions()
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '请求失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHistoryPreviewUrls(row: AdminHistoryItem) {
|
|
||||||
const urls = row.long_image_url ? [row.long_image_url] : []
|
|
||||||
return urls.concat(row.result_urls || []).slice(0, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTabChange(name: string | number) {
|
|
||||||
if (name === 'history') {
|
|
||||||
loadHistory(1).catch(() => undefined)
|
|
||||||
loadUserOptions().catch(() => undefined)
|
|
||||||
} else {
|
|
||||||
loadUsers(1).catch(() => undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadUsers(1)
|
|
||||||
await loadUserOptions()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.admin-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 24px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-tabs {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 360px 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-card,
|
|
||||||
.table-card {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-tip {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar > * {
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: 16px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-card {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-preview {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-image {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.admin-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,166 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="light">
|
|
||||||
<div class="home-page">
|
|
||||||
<header class="home-header">
|
|
||||||
<div class="brand">数富AI</div>
|
|
||||||
<div class="header-actions">
|
|
||||||
<span>{{ username }}</span>
|
|
||||||
<el-popover placement="bottom-end" :width="320" trigger="click">
|
|
||||||
<template #reference>
|
|
||||||
<el-button circle>↻</el-button>
|
|
||||||
</template>
|
|
||||||
<div class="update-panel">
|
|
||||||
<div class="update-title">软件更新</div>
|
|
||||||
<div class="update-version">当前版本: v{{ version }}</div>
|
|
||||||
<el-button type="primary" text :loading="loading" @click="check">检测更新</el-button>
|
|
||||||
<div class="update-hint">{{ hint }}</div>
|
|
||||||
<el-button v-if="downloadVisible" type="success" :loading="downloadLoading" @click="handleUpdate">
|
|
||||||
立即更新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-popover>
|
|
||||||
<a href="/login" @click.prevent="handleLogout">退出</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="home-main">
|
|
||||||
<div class="entrances">
|
|
||||||
<router-link v-if="isVisible('brand')" class="entrance-card" to="/brand">亚马逊</router-link>
|
|
||||||
<button v-if="isVisible('wb')" class="entrance-card is-disabled" type="button" @click="showComingSoon">
|
|
||||||
wildberries
|
|
||||||
</button>
|
|
||||||
<router-link v-if="isVisible('image')" class="entrance-card" to="/image">图片</router-link>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { logout } from '@/shared/api/auth'
|
|
||||||
import { useUpdateCheck } from '@/shared/composables/useUpdateCheck'
|
|
||||||
import { getBootstrapState } from '@/shared/utils/bootstrap'
|
|
||||||
import { getUserColumnPermissions } from '@/shared/api/admin'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const bootstrap = getBootstrapState()
|
|
||||||
const username = computed(() => bootstrap.username || '用户')
|
|
||||||
const userId = computed(() => bootstrap.userId || '')
|
|
||||||
const visibleKeys = ref<string[]>(['brand', 'wb', 'image'])
|
|
||||||
const { loading, version, hint, downloadVisible, downloadLoading, check, runUpdate } = useUpdateCheck()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!userId.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await getUserColumnPermissions(userId.value)
|
|
||||||
if (result.success && Array.isArray(result.items)) {
|
|
||||||
visibleKeys.value = result.items
|
|
||||||
.map((item) => (item.column_key || '').toLowerCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// keep default entrances if permissions cannot be loaded
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isVisible = (key: string) => visibleKeys.value.includes(key)
|
|
||||||
|
|
||||||
const showComingSoon = () => {
|
|
||||||
ElMessage.info('暂未开通,敬请期待')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await logout()
|
|
||||||
localStorage.removeItem('maixiang_api_key')
|
|
||||||
window.__BOOTSTRAP__ = {}
|
|
||||||
await router.replace('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
|
||||||
const confirmed = window.confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')
|
|
||||||
if (!confirmed) return
|
|
||||||
await runUpdate()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.home-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background:
|
|
||||||
linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)),
|
|
||||||
url('/static/bg.jpg') center/cover no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-main {
|
|
||||||
min-height: calc(100vh - 72px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entrances {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entrance-card {
|
|
||||||
width: 180px;
|
|
||||||
height: 110px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-disabled {
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-version,
|
|
||||||
.update-hint {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="dark">
|
|
||||||
<div class="image-page">
|
|
||||||
<header class="top-bar">
|
|
||||||
<div class="logo-area">
|
|
||||||
<span class="app-name">数富AI</span>
|
|
||||||
<router-link to="/home" class="btn-home">返回首页</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-tabs">
|
|
||||||
<div class="nav-tab-group">
|
|
||||||
<button type="button" class="nav-tab active">图片</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="page-body"></main>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
document.title = '图片 - 数富AI'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.image-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: rgba(13, 13, 13, 1);
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-family: "Microsoft YaHei", "Noto Serif SC", "SimSun", "Songti SC", serif;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-bar {
|
|
||||||
height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-bottom: 1px solid #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-area {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-name {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-home {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #999;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-home:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(77, 72, 72, 0.99);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab {
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #3498db;
|
|
||||||
background: rgba(52, 152, 219, 0.2);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-body {
|
|
||||||
height: calc(100vh - 56px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageShell theme="light">
|
|
||||||
<div class="login-page">
|
|
||||||
<div class="login-header">数富AI</div>
|
|
||||||
<el-card class="login-card" shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="login-title">登录</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" class="login-alert" />
|
|
||||||
|
|
||||||
<el-form label-width="72px" @submit.prevent="handleSubmit">
|
|
||||||
<el-form-item label="用户名">
|
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名" autofocus />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="密码">
|
|
||||||
<el-input v-model="form.password" type="password" show-password placeholder="请输入密码"
|
|
||||||
@keydown.enter="handleSubmit" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-button type="primary" :loading="submitting" class="login-button" @click="handleSubmit">
|
|
||||||
登录
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</PageShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { checkAuth, login } from '@/shared/api/auth'
|
|
||||||
import { getBootstrapState } from '@/shared/utils/bootstrap'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const bootstrap = getBootstrapState()
|
|
||||||
const submitting = ref(false)
|
|
||||||
const errorMessage = ref(bootstrap.error || '')
|
|
||||||
const form = reactive({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const result = await checkAuth()
|
|
||||||
if (!result.logged_in) return
|
|
||||||
|
|
||||||
const target = result.redirect || '/home'
|
|
||||||
if (target.startsWith('/')) {
|
|
||||||
await router.push(target)
|
|
||||||
} else {
|
|
||||||
window.location.href = target
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore auth check failures during bootstrap
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!form.username || !form.password) {
|
|
||||||
errorMessage.value = '请输入用户名和密码'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
errorMessage.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await login(form.username, form.password)
|
|
||||||
if (result.success) {
|
|
||||||
const target = result.redirect || '/home'
|
|
||||||
if (target.startsWith('/')) {
|
|
||||||
await router.push(target)
|
|
||||||
} else {
|
|
||||||
window.location.href = target
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage.value = result.error || '登录失败'
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage.value = error instanceof Error ? error.message : '登录失败'
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.login-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px 16px;
|
|
||||||
background: linear-gradient(180deg, #dce8f1 0%, #eef4f8 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.6);
|
|
||||||
color: #303133;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 420px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-alert {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
redirect: '/home',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
component: () => import('@/pages/login/index.vue'),
|
|
||||||
meta: { title: '登录 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home',
|
|
||||||
component: () => import('@/pages/home/index.vue'),
|
|
||||||
meta: { title: '首页 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/brand',
|
|
||||||
component: () => import('@/pages/brand/index.vue'),
|
|
||||||
meta: { title: '亚马逊 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin',
|
|
||||||
component: () => import('@/pages/admin/index.vue'),
|
|
||||||
meta: { title: '管理后台 - 数富AI' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/image',
|
|
||||||
component: () => import('@/pages/image/index.vue'),
|
|
||||||
meta: { title: '图片 - 数富AI' },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(),
|
|
||||||
routes,
|
|
||||||
})
|
|
||||||
|
|
||||||
router.afterEach((to) => {
|
|
||||||
if (to.meta.title) {
|
|
||||||
document.title = String(to.meta.title)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { requestDeleteJson, requestGetJson, requestPostJson, requestPutJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface AdminUserItem {
|
|
||||||
id: number
|
|
||||||
username?: string
|
|
||||||
role?: 'super_admin' | 'admin' | 'normal' | string
|
|
||||||
creator_username?: string
|
|
||||||
created_at?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminSimpleUser {
|
|
||||||
id: number
|
|
||||||
username: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminUsersResponse {
|
|
||||||
success: boolean
|
|
||||||
current_user_role?: 'super_admin' | 'admin' | 'normal' | string
|
|
||||||
admins?: AdminSimpleUser[]
|
|
||||||
items?: AdminUserItem[]
|
|
||||||
total?: number
|
|
||||||
page?: number
|
|
||||||
page_size?: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminMutationResponse {
|
|
||||||
success: boolean
|
|
||||||
msg?: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminHistoryItem {
|
|
||||||
id: number
|
|
||||||
username?: string
|
|
||||||
panel_type?: string
|
|
||||||
created_at?: string
|
|
||||||
long_image_url?: string | null
|
|
||||||
result_urls?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminHistoryResponse {
|
|
||||||
success: boolean
|
|
||||||
items?: AdminHistoryItem[]
|
|
||||||
total?: number
|
|
||||||
page?: number
|
|
||||||
page_size?: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminColumnPermissionItem {
|
|
||||||
column_key?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminColumnPermissionResponse {
|
|
||||||
success: boolean
|
|
||||||
items?: AdminColumnPermissionItem[]
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdminUsers(query = '') {
|
|
||||||
const suffix = query ? `?${query}` : ''
|
|
||||||
return requestGetJson<AdminUsersResponse>(`/api/admin/users${suffix}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAdminUser(payload: {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
role: string
|
|
||||||
created_by_id?: number
|
|
||||||
}) {
|
|
||||||
return requestPostJson<AdminMutationResponse>('/api/admin/user', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateAdminUser(
|
|
||||||
userId: number | string,
|
|
||||||
payload: {
|
|
||||||
password?: string
|
|
||||||
role?: string
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return requestPutJson<AdminMutationResponse>(`/api/admin/user/${userId}`, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteAdminUser(userId: number | string) {
|
|
||||||
return requestDeleteJson<AdminMutationResponse>(`/api/admin/user/${userId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdminHistory(query = '') {
|
|
||||||
const suffix = query ? `?${query}` : ''
|
|
||||||
return requestGetJson<AdminHistoryResponse>(`/api/admin/history${suffix}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserColumnPermissions(userId: string | number) {
|
|
||||||
return requestGetJson<AdminColumnPermissionResponse>(`/api/admin/user/${userId}/column-permissions`)
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface LoginResponse {
|
|
||||||
success: boolean
|
|
||||||
redirect?: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthCheckResponse {
|
|
||||||
logged_in: boolean
|
|
||||||
redirect?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkAuth() {
|
|
||||||
return requestGetJson<AuthCheckResponse>('/api/auth/check')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function login(username: string, password: string) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.set('username', username)
|
|
||||||
formData.set('password', password)
|
|
||||||
|
|
||||||
return requestPostJson<LoginResponse>('/login', formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function logout() {
|
|
||||||
try {
|
|
||||||
await requestGetJson('/logout', {
|
|
||||||
validateStatus: () => true,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
// logout should still continue on the client even if backend responds abnormally
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface UploadFileVo {
|
|
||||||
fileKey: string
|
|
||||||
originalFilename?: string
|
|
||||||
localPath?: string
|
|
||||||
size?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConvertTemplateVo {
|
|
||||||
id: number
|
|
||||||
templateCode: string
|
|
||||||
templateName: string
|
|
||||||
outputFilename: string
|
|
||||||
isDefault?: boolean
|
|
||||||
builtIn?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UploadedSourceFileDto {
|
|
||||||
fileKey: string
|
|
||||||
originalFilename?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConvertResultItemVo {
|
|
||||||
resultId?: number
|
|
||||||
sourceFilename?: string
|
|
||||||
outputFilename?: string
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
downloadUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConvertRunVo {
|
|
||||||
total: number
|
|
||||||
successCount: number
|
|
||||||
failedCount: number
|
|
||||||
items: ConvertResultItemVo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StandardApiResponse<T> {
|
|
||||||
success: boolean
|
|
||||||
message?: string
|
|
||||||
data?: T
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConvertTemplates() {
|
|
||||||
return requestGetJson<StandardApiResponse<ConvertTemplateVo[]>>('/api/convert/templates')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uploadTempFile(file: File) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
return requestPostJson<StandardApiResponse<UploadFileVo>, FormData>('/api/files/upload', formData, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runConvert(files: UploadedSourceFileDto[], templateId: number) {
|
|
||||||
return requestPostJson<StandardApiResponse<ConvertRunVo>>('/api/convert/run', {
|
|
||||||
files,
|
|
||||||
templateId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { requestGetJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface DedupeHistoryItem {
|
|
||||||
sourceFilename?: string
|
|
||||||
outputFilename?: string
|
|
||||||
downloadUrl?: string
|
|
||||||
success: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DedupeHistoryVo {
|
|
||||||
items: DedupeHistoryItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StandardApiResponse<T> {
|
|
||||||
success: boolean
|
|
||||||
message?: string
|
|
||||||
data?: T
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDedupeHistory() {
|
|
||||||
return requestGetJson<StandardApiResponse<DedupeHistoryVo>>('/api/dedupe/history')
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { requestGetJson, requestPostJson } from '@/shared/api/http'
|
|
||||||
|
|
||||||
export interface VersionResponse {
|
|
||||||
version?: string
|
|
||||||
has_update?: boolean
|
|
||||||
latest_version?: string
|
|
||||||
desc?: string
|
|
||||||
file_url?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateStartResponse {
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchVersion() {
|
|
||||||
return requestGetJson<VersionResponse>('/api/version')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startUpdate(fileUrl: string) {
|
|
||||||
return requestPostJson<UpdateStartResponse>('/api/update/do', { file_url: fileUrl })
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ export interface SplitExcelPayload {
|
|||||||
split_mode: 'rows_per_file' | 'parts'
|
split_mode: 'rows_per_file' | 'parts'
|
||||||
rows_per_file?: number
|
rows_per_file?: number
|
||||||
parts?: number
|
parts?: number
|
||||||
output_dir: string
|
output_dir?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SplitExcelResultItem {
|
export interface SplitExcelResultItem {
|
||||||
@@ -37,6 +37,14 @@ export interface ExcelInfoResponse {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CleanExcelPayload {
|
||||||
|
paths: string[]
|
||||||
|
selected_columns: string[]
|
||||||
|
keep_integer_ids?: boolean
|
||||||
|
keep_underscore_ids?: boolean
|
||||||
|
output_dir?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface CleanExcelResultItem {
|
export interface CleanExcelResultItem {
|
||||||
result_id?: number
|
result_id?: number
|
||||||
source_path: string
|
source_path: string
|
||||||
@@ -47,7 +55,7 @@ export interface CleanExcelResultItem {
|
|||||||
|
|
||||||
export interface ConvertTxtPayload {
|
export interface ConvertTxtPayload {
|
||||||
paths: string[]
|
paths: string[]
|
||||||
output_dir: string
|
output_dir?: string
|
||||||
template_id: string
|
template_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
import { fetchVersion, startUpdate } from '@/shared/api/update'
|
|
||||||
|
|
||||||
export function useUpdateCheck() {
|
|
||||||
const loading = ref(false)
|
|
||||||
const version = ref('1.0.0')
|
|
||||||
const hint = ref('')
|
|
||||||
const downloadVisible = ref(false)
|
|
||||||
const downloadLoading = ref(false)
|
|
||||||
const fileUrl = ref('')
|
|
||||||
|
|
||||||
const check = async () => {
|
|
||||||
loading.value = true
|
|
||||||
hint.value = '正在检测更新...'
|
|
||||||
downloadVisible.value = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fetchVersion()
|
|
||||||
version.value = (result.version || version.value).replace(/^v/i, '')
|
|
||||||
|
|
||||||
if (result.has_update && result.latest_version) {
|
|
||||||
hint.value = `发现新版本 v${result.latest_version}${result.desc ? `:${result.desc}` : ''}`
|
|
||||||
fileUrl.value = result.file_url || ''
|
|
||||||
downloadVisible.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hint.value = '已是最新版本'
|
|
||||||
} catch (error) {
|
|
||||||
hint.value = error instanceof Error ? error.message : '检测更新失败'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runUpdate = async () => {
|
|
||||||
if (!fileUrl.value) {
|
|
||||||
hint.value = '暂无下载地址,请关注官方渠道。'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadLoading.value = true
|
|
||||||
hint.value = '正在下载并准备更新,程序将自动退出...'
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await startUpdate(fileUrl.value)
|
|
||||||
if (result.success) {
|
|
||||||
hint.value = '更新已启动,程序即将退出...'
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
hint.value = result.error || '更新启动失败'
|
|
||||||
return false
|
|
||||||
} catch (error) {
|
|
||||||
hint.value = error instanceof Error ? error.message : '请求更新失败,请重试'
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
downloadLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
version,
|
|
||||||
hint,
|
|
||||||
downloadVisible,
|
|
||||||
downloadLoading,
|
|
||||||
check,
|
|
||||||
runUpdate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import { createApp } from 'vue'
|
|||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import '@/styles/main.css'
|
import '@/styles/main.css'
|
||||||
import App from '@/App.vue'
|
import BrandSplitTab from '@/pages/brand/components/BrandSplitTab.vue'
|
||||||
import router from '@/router'
|
|
||||||
|
|
||||||
createApp(App).use(ElementPlus).use(router).mount('#app')
|
createApp(BrandSplitTab).use(ElementPlus).mount('#app')
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import AutoImport from 'unplugin-auto-import/vite'
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
@@ -6,6 +7,7 @@ import Components from 'unplugin-vue-components/vite'
|
|||||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
@@ -47,5 +49,19 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
cssCodeSplit: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
dedupe: resolve(__dirname, 'dedupe.html'),
|
||||||
|
convert: resolve(__dirname, 'convert.html'),
|
||||||
|
split: resolve(__dirname, 'split.html'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'assets/[name].js',
|
||||||
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,73 +1,73 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import alibabacloud_oss_v2 as oss
|
import alibabacloud_oss_v2 as oss
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from config import region, endpoint, bucket, file_url_pre, bucket_path
|
from config import region, endpoint, bucket, file_url_pre, bucket_path
|
||||||
|
|
||||||
|
|
||||||
def upload_file(file_content: bytes, key: str):
|
def upload_file(file_content: bytes, key: str):
|
||||||
credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
|
credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
|
||||||
cfg = oss.config.load_default()
|
cfg = oss.config.load_default()
|
||||||
cfg.credentials_provider = credentials_provider
|
cfg.credentials_provider = credentials_provider
|
||||||
cfg.region = region
|
cfg.region = region
|
||||||
cfg.endpoint = endpoint
|
cfg.endpoint = endpoint
|
||||||
cfg.retry_max_attempts = 3
|
cfg.retry_max_attempts = 3
|
||||||
client = oss.Client(cfg)
|
client = oss.Client(cfg)
|
||||||
|
|
||||||
result = client.put_object(
|
result = client.put_object(
|
||||||
oss.PutObjectRequest(
|
oss.PutObjectRequest(
|
||||||
bucket=bucket, # 存储空间名称
|
bucket=bucket, # 存储空间名称
|
||||||
key=key, # 对象名称
|
key=key, # 对象名称
|
||||||
body=file_content # 读取文件内容
|
body=file_content # 读取文件内容
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# print(result)
|
# print(result)
|
||||||
return file_url_pre + key
|
return file_url_pre + key
|
||||||
|
|
||||||
|
|
||||||
def upload_data_url(data_url: str, prefix: str = "history", key_hint: str = "") -> str:
|
def upload_data_url(data_url: str, prefix: str = "history", key_hint: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
将 base64 data URL 上传到 OSS,返回图片链接
|
将 base64 data URL 上传到 OSS,返回图片链接
|
||||||
data_url: data:image/png;base64,xxxx 或 data:image/jpeg;base64,xxxx
|
data_url: data:image/png;base64,xxxx 或 data:image/jpeg;base64,xxxx
|
||||||
prefix: OSS key 前缀
|
prefix: OSS key 前缀
|
||||||
key_hint: 可选后缀避免重名,如 "_0", "_1"
|
key_hint: 可选后缀避免重名,如 "_0", "_1"
|
||||||
"""
|
"""
|
||||||
match = re.match(r'data:image/(\w+);base64,(.+)', data_url)
|
match = re.match(r'data:image/(\w+);base64,(.+)', data_url)
|
||||||
if not match:
|
if not match:
|
||||||
raise ValueError('无效的 data URL 格式')
|
raise ValueError('无效的 data URL 格式')
|
||||||
ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg'
|
ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg'
|
||||||
file_content = base64.b64decode(match.group(2))
|
file_content = base64.b64decode(match.group(2))
|
||||||
ts = int(time.time() * 1000)
|
ts = int(time.time() * 1000)
|
||||||
key = f"{bucket_path}{prefix}/{ts}{key_hint}.{ext}"
|
key = f"{bucket_path}{prefix}/{ts}{key_hint}.{ext}"
|
||||||
return upload_file(file_content, key)
|
return upload_file(file_content, key)
|
||||||
|
|
||||||
|
|
||||||
def upload_data_urls(data_urls: list, prefix: str = "history") -> list:
|
def upload_data_urls(data_urls: list, prefix: str = "history") -> list:
|
||||||
"""批量上传 base64 图片到 OSS,返回图片链接列表"""
|
"""批量上传 base64 图片到 OSS,返回图片链接列表"""
|
||||||
urls = []
|
urls = []
|
||||||
ts = int(time.time() * 1000)
|
ts = int(time.time() * 1000)
|
||||||
for i, data_url in enumerate(data_urls or []):
|
for i, data_url in enumerate(data_urls or []):
|
||||||
if not data_url or not isinstance(data_url, str):
|
if not data_url or not isinstance(data_url, str):
|
||||||
continue
|
continue
|
||||||
if not data_url.startswith("http"):
|
if not data_url.startswith("http"):
|
||||||
match = re.match(r'data:image/(\w+);base64,(.+)', data_url)
|
match = re.match(r'data:image/(\w+);base64,(.+)', data_url)
|
||||||
if not match:
|
if not match:
|
||||||
continue
|
continue
|
||||||
ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg'
|
ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg'
|
||||||
file_content = base64.b64decode(match.group(2))
|
file_content = base64.b64decode(match.group(2))
|
||||||
else:
|
else:
|
||||||
file_content = requests.get(data_url).content
|
file_content = requests.get(data_url).content
|
||||||
ext = "png"
|
ext = "png"
|
||||||
key = f"{bucket_path}{prefix}/{ts}_{i}.{ext}"
|
key = f"{bucket_path}{prefix}/{ts}_{i}.{ext}"
|
||||||
urls.append(upload_file(file_content, key))
|
urls.append(upload_file(file_content, key))
|
||||||
return urls
|
return urls
|
||||||
|
|
||||||
|
|
||||||
# 脚本入口,当文件被直接运行时调用main函数
|
# 脚本入口,当文件被直接运行时调用main函数
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
with open("测试图片数据/IMG_2685.JPG", "rb") as f:
|
with open("测试图片数据/IMG_2685.JPG", "rb") as f:
|
||||||
file_content = f.read()
|
file_content = f.read()
|
||||||
upload_file(file_content,key=bucket_path+"test.png")
|
upload_file(file_content,key=bucket_path+"test.png")
|
||||||
@@ -1,261 +1,261 @@
|
|||||||
"""
|
"""
|
||||||
公共模块:数据库连接、初始化、会话校验、装饰器、模板渲染
|
公共模块:数据库连接、初始化、会话校验、装饰器、模板渲染
|
||||||
供各蓝图复用
|
供各蓝图复用
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
from flask import request, redirect, url_for, session, jsonify, render_template, render_template_string
|
from flask import request, redirect, url_for, session, jsonify, render_template, render_template_string
|
||||||
|
|
||||||
from config import mysql_host, mysql_user, mysql_password, mysql_database
|
from config import mysql_host, mysql_user, mysql_password, mysql_database
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
STATIC_DIR = os.path.join(BASE_DIR, 'static')
|
STATIC_DIR = os.path.join(BASE_DIR, 'static')
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
return pymysql.connect(
|
return pymysql.connect(
|
||||||
host=mysql_host,
|
host=mysql_host,
|
||||||
user=mysql_user,
|
user=mysql_user,
|
||||||
password=mysql_password,
|
password=mysql_password,
|
||||||
database=mysql_database,
|
database=mysql_database,
|
||||||
charset='utf8mb4',
|
charset='utf8mb4',
|
||||||
cursorclass=pymysql.cursors.DictCursor
|
cursorclass=pymysql.cursors.DictCursor
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _render_html(template_name: str, **context):
|
def _render_html(template_name: str, **context):
|
||||||
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
|
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
|
||||||
path = os.path.join(BASE_DIR, "web_source", template_name)
|
path = os.path.join(BASE_DIR, "web_source", template_name)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
return render_template(template_name, **context)
|
return render_template(template_name, **context)
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
try:
|
try:
|
||||||
from html_crypto import decrypt
|
from html_crypto import decrypt
|
||||||
content = decrypt(raw).decode("utf-8")
|
content = decrypt(raw).decode("utf-8")
|
||||||
except Exception:
|
except Exception:
|
||||||
content = raw.decode("utf-8", errors="replace")
|
content = raw.decode("utf-8", errors="replace")
|
||||||
return render_template_string(content, **context)
|
return render_template_string(content, **context)
|
||||||
|
|
||||||
|
|
||||||
def _is_session_user_valid():
|
def _is_session_user_valid():
|
||||||
"""校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False"""
|
"""校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False"""
|
||||||
uid = session.get('user_id')
|
uid = session.get('user_id')
|
||||||
if not uid:
|
if not uid:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT id FROM users WHERE id = %s", (uid,))
|
cur.execute("SELECT id FROM users WHERE id = %s", (uid,))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not row:
|
if not row:
|
||||||
session.clear()
|
session.clear()
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
session.clear()
|
session.clear()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _get_current_admin_role():
|
def _get_current_admin_role():
|
||||||
"""获取当前登录用户的管理角色:super_admin / admin / None(非管理员)"""
|
"""获取当前登录用户的管理角色:super_admin / admin / None(非管理员)"""
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
|
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
|
||||||
(session['user_id'],)
|
(session['user_id'],)
|
||||||
)
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not row or not row.get('is_admin'):
|
if not row or not row.get('is_admin'):
|
||||||
return None, None
|
return None, None
|
||||||
return row.get('role') or ('super_admin' if row.get('created_by_id') is None else 'admin'), row
|
return row.get('role') or ('super_admin' if row.get('created_by_id') is None else 'admin'), row
|
||||||
except Exception:
|
except Exception:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def login_required(f):
|
def login_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if not session.get('user_id') or not _is_session_user_valid():
|
if not session.get('user_id') or not _is_session_user_valid():
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def admin_required(f):
|
def admin_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if not session.get('user_id'):
|
if not session.get('user_id'):
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (session['user_id'],))
|
cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (session['user_id'],))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not row or not row.get('is_admin'):
|
if not row or not row.get('is_admin'):
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||||
return redirect(url_for('main.home'))
|
return redirect(url_for('main.home'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
return redirect(url_for('main.home'))
|
return redirect(url_for('main.home'))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""初始化数据库表,若不存在则创建"""
|
"""初始化数据库表,若不存在则创建"""
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
conn = pymysql.connect(
|
conn = pymysql.connect(
|
||||||
host=mysql_host,
|
host=mysql_host,
|
||||||
user=mysql_user,
|
user=mysql_user,
|
||||||
password=mysql_password,
|
password=mysql_password,
|
||||||
charset='utf8mb4'
|
charset='utf8mb4'
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{mysql_database}` DEFAULT CHARSET utf8mb4")
|
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{mysql_database}` DEFAULT CHARSET utf8mb4")
|
||||||
cur.execute(f"USE `{mysql_database}`")
|
cur.execute(f"USE `{mysql_database}`")
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
username VARCHAR(64) NOT NULL UNIQUE,
|
username VARCHAR(64) NOT NULL UNIQUE,
|
||||||
password_hash VARCHAR(256) NOT NULL,
|
password_hash VARCHAR(256) NOT NULL,
|
||||||
is_admin TINYINT(1) DEFAULT 0,
|
is_admin TINYINT(1) DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS image_history (
|
CREATE TABLE IF NOT EXISTS image_history (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
panel_type VARCHAR(64) DEFAULT '',
|
panel_type VARCHAR(64) DEFAULT '',
|
||||||
original_urls JSON,
|
original_urls JSON,
|
||||||
params JSON,
|
params JSON,
|
||||||
result_urls JSON,
|
result_urls JSON,
|
||||||
long_image_url VARCHAR(1024) DEFAULT NULL,
|
long_image_url VARCHAR(1024) DEFAULT NULL,
|
||||||
INDEX idx_user_created (user_id, created_at DESC)
|
INDEX idx_user_created (user_id, created_at DESC)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE image_history ADD COLUMN long_image_url VARCHAR(1024) DEFAULT NULL")
|
cur.execute("ALTER TABLE image_history ADD COLUMN long_image_url VARCHAR(1024) DEFAULT NULL")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE users ADD COLUMN machine VARCHAR(64) DEFAULT NULL")
|
cur.execute("ALTER TABLE users ADD COLUMN machine VARCHAR(64) DEFAULT NULL")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'normal'")
|
cur.execute("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'normal'")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE users ADD COLUMN created_by_id INT NULL")
|
cur.execute("ALTER TABLE users ADD COLUMN created_by_id INT NULL")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS brand_crawl_tasks (
|
CREATE TABLE IF NOT EXISTS brand_crawl_tasks (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
file_paths JSON NOT NULL,
|
file_paths JSON NOT NULL,
|
||||||
status VARCHAR(20) DEFAULT 'pending',
|
status VARCHAR(20) DEFAULT 'pending',
|
||||||
task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行',
|
task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行',
|
||||||
result_paths JSON NULL,
|
result_paths JSON NULL,
|
||||||
error_message TEXT NULL,
|
error_message TEXT NULL,
|
||||||
progress_current INT DEFAULT 0,
|
progress_current INT DEFAULT 0,
|
||||||
progress_total INT DEFAULT 0,
|
progress_total INT DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
INDEX idx_user_status (user_id, status)
|
INDEX idx_user_status (user_id, status)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_current INT DEFAULT 0")
|
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_current INT DEFAULT 0")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_total INT DEFAULT 0")
|
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_total INT DEFAULT 0")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN `desc` VARCHAR(500) NULL COMMENT '上传文件名描述'")
|
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN `desc` VARCHAR(500) NULL COMMENT '上传文件名描述'")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN strategy VARCHAR(20) DEFAULT 'Terms' COMMENT '品牌匹配方式: Terms/Simple'")
|
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN strategy VARCHAR(20) DEFAULT 'Terms' COMMENT '品牌匹配方式: Terms/Simple'")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行'")
|
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行'")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS columns (
|
CREATE TABLE IF NOT EXISTS columns (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(128) NOT NULL COMMENT '栏目名',
|
name VARCHAR(128) NOT NULL COMMENT '栏目名',
|
||||||
column_key VARCHAR(64) NOT NULL COMMENT '栏目标识',
|
column_key VARCHAR(64) NOT NULL COMMENT '栏目标识',
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_column_key (column_key)
|
UNIQUE KEY uk_column_key (column_key)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS user_column_permission (
|
CREATE TABLE IF NOT EXISTS user_column_permission (
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
column_id INT NOT NULL,
|
column_id INT NOT NULL,
|
||||||
PRIMARY KEY (user_id, column_id),
|
PRIMARY KEY (user_id, column_id),
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
|
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
try:
|
try:
|
||||||
cur.execute("UPDATE users SET role = 'normal' WHERE (role IS NULL OR role = '') AND (is_admin = 0 OR is_admin IS NULL)")
|
cur.execute("UPDATE users SET role = 'normal' WHERE (role IS NULL OR role = '') AND (is_admin = 0 OR is_admin IS NULL)")
|
||||||
cur.execute("SELECT MIN(id) AS mid FROM users WHERE is_admin = 1")
|
cur.execute("SELECT MIN(id) AS mid FROM users WHERE is_admin = 1")
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if row and row.get('mid'):
|
if row and row.get('mid'):
|
||||||
mid = row['mid']
|
mid = row['mid']
|
||||||
cur.execute("UPDATE users SET role = 'super_admin' WHERE id = %s", (mid,))
|
cur.execute("UPDATE users SET role = 'super_admin' WHERE id = %s", (mid,))
|
||||||
cur.execute("UPDATE users SET role = 'admin' WHERE is_admin = 1 AND id != %s", (mid,))
|
cur.execute("UPDATE users SET role = 'admin' WHERE is_admin = 1 AND id != %s", (mid,))
|
||||||
cur.execute("UPDATE users SET created_by_id = %s WHERE role = 'admin' AND (created_by_id IS NULL)", (mid,))
|
cur.execute("UPDATE users SET created_by_id = %s WHERE role = 'admin' AND (created_by_id IS NULL)", (mid,))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
_create_initial_admin()
|
_create_initial_admin()
|
||||||
|
|
||||||
|
|
||||||
def _create_initial_admin():
|
def _create_initial_admin():
|
||||||
"""若没有任何管理员,则创建默认超级管理员(首次启动时,仅一个)"""
|
"""若没有任何管理员,则创建默认超级管理员(首次启动时,仅一个)"""
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT id FROM users WHERE role = 'super_admin' LIMIT 1")
|
cur.execute("SELECT id FROM users WHERE role = 'super_admin' LIMIT 1")
|
||||||
if cur.fetchone():
|
if cur.fetchone():
|
||||||
conn.close()
|
conn.close()
|
||||||
return
|
return
|
||||||
admin_user = os.environ.get('ADMIN_USER', 'admin')
|
admin_user = os.environ.get('ADMIN_USER', 'admin')
|
||||||
admin_pwd = os.environ.get('ADMIN_PASSWORD', 'admin123')
|
admin_pwd = os.environ.get('ADMIN_PASSWORD', 'admin123')
|
||||||
pwd_hash = generate_password_hash(admin_pwd, method='pbkdf2:sha256')
|
pwd_hash = generate_password_hash(admin_pwd, method='pbkdf2:sha256')
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT INTO users (username, password_hash, is_admin, role) VALUES (%s, %s, 1, 'super_admin')",
|
"INSERT INTO users (username, password_hash, is_admin, role) VALUES (%s, %s, 1, 'super_admin')",
|
||||||
(admin_user, pwd_hash)
|
(admin_user, pwd_hash)
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -1 +1 @@
|
|||||||
# 蓝图包
|
# 蓝图包
|
||||||
@@ -1,362 +1,362 @@
|
|||||||
"""
|
"""
|
||||||
管理员蓝图:用户管理(列表/创建/更新/删除)、生成历史、管理页
|
管理员蓝图:用户管理(列表/创建/更新/删除)、生成历史、管理页
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import pymysql
|
import pymysql
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from app_common import get_db, _render_html, _get_current_admin_role, admin_required, login_required
|
from app_common import get_db, _render_html, _get_current_admin_role, admin_required, login_required
|
||||||
from flask import session
|
from flask import session
|
||||||
|
|
||||||
admin_bp = Blueprint('admin', __name__)
|
admin_bp = Blueprint('admin', __name__)
|
||||||
|
|
||||||
|
|
||||||
def _parse_json(val, default=None):
|
def _parse_json(val, default=None):
|
||||||
if val is None:
|
if val is None:
|
||||||
return default if default is not None else []
|
return default if default is not None else []
|
||||||
if isinstance(val, (list, dict)):
|
if isinstance(val, (list, dict)):
|
||||||
return val
|
return val
|
||||||
try:
|
try:
|
||||||
return json.loads(val)
|
return json.loads(val)
|
||||||
except Exception:
|
except Exception:
|
||||||
return default if default is not None else []
|
return default if default is not None else []
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/admin')
|
@admin_bp.route('/admin')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_page():
|
def admin_page():
|
||||||
return _render_html('admin.html')
|
return _render_html('admin.html')
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/api/admin/users')
|
@admin_bp.route('/api/admin/users')
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_list_users():
|
def admin_list_users():
|
||||||
"""分页获取用户列表;支持用户名模糊搜索、指定管理员所属普通用户筛选"""
|
"""分页获取用户列表;支持用户名模糊搜索、指定管理员所属普通用户筛选"""
|
||||||
role, current_row = _get_current_admin_role()
|
role, current_row = _get_current_admin_role()
|
||||||
if not role:
|
if not role:
|
||||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||||
page = max(1, int(request.args.get('page', 1)))
|
page = max(1, int(request.args.get('page', 1)))
|
||||||
page_size = min(50, max(5, int(request.args.get('page_size', 15))))
|
page_size = min(50, max(5, int(request.args.get('page_size', 15))))
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
search_username = (request.args.get('username') or request.args.get('search') or '').strip()
|
search_username = (request.args.get('username') or request.args.get('search') or '').strip()
|
||||||
created_by_id_arg = request.args.get('created_by_id') or request.args.get('admin_id')
|
created_by_id_arg = request.args.get('created_by_id') or request.args.get('admin_id')
|
||||||
created_by_id = int(created_by_id_arg) if created_by_id_arg and str(created_by_id_arg).isdigit() else None
|
created_by_id = int(created_by_id_arg) if created_by_id_arg and str(created_by_id_arg).isdigit() else None
|
||||||
if role != 'super_admin':
|
if role != 'super_admin':
|
||||||
created_by_id = None
|
created_by_id = None
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
if role == 'super_admin':
|
if role == 'super_admin':
|
||||||
where_parts = ["1=1"]
|
where_parts = ["1=1"]
|
||||||
params = []
|
params = []
|
||||||
if search_username:
|
if search_username:
|
||||||
where_parts.append("u.username LIKE %s")
|
where_parts.append("u.username LIKE %s")
|
||||||
params.append("%" + search_username + "%")
|
params.append("%" + search_username + "%")
|
||||||
if created_by_id is not None:
|
if created_by_id is not None:
|
||||||
where_parts.append("u.created_by_id = %s")
|
where_parts.append("u.created_by_id = %s")
|
||||||
params.append(created_by_id)
|
params.append(created_by_id)
|
||||||
where_sql = " AND ".join(where_parts)
|
where_sql = " AND ".join(where_parts)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id,
|
"""SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id,
|
||||||
creator.username AS creator_username
|
creator.username AS creator_username
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN users creator ON creator.id = u.created_by_id
|
LEFT JOIN users creator ON creator.id = u.created_by_id
|
||||||
WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""",
|
WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""",
|
||||||
tuple(params) + (page_size, offset),
|
tuple(params) + (page_size, offset),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.execute("SELECT COUNT(*) as total FROM users u WHERE " + where_sql, tuple(params))
|
cur.execute("SELECT COUNT(*) as total FROM users u WHERE " + where_sql, tuple(params))
|
||||||
total = cur.fetchone()['total']
|
total = cur.fetchone()['total']
|
||||||
cur.execute("SELECT id, username FROM users WHERE role = 'admin' ORDER BY id")
|
cur.execute("SELECT id, username FROM users WHERE role = 'admin' ORDER BY id")
|
||||||
admins = [{'id': r['id'], 'username': r['username']} for r in cur.fetchall()]
|
admins = [{'id': r['id'], 'username': r['username']} for r in cur.fetchall()]
|
||||||
else:
|
else:
|
||||||
admin_id = current_row['id']
|
admin_id = current_row['id']
|
||||||
where_parts = ["(u.id = %s OR (u.role = 'normal' AND u.created_by_id = %s))"]
|
where_parts = ["(u.id = %s OR (u.role = 'normal' AND u.created_by_id = %s))"]
|
||||||
params = [admin_id, admin_id]
|
params = [admin_id, admin_id]
|
||||||
if search_username:
|
if search_username:
|
||||||
where_parts.append("u.username LIKE %s")
|
where_parts.append("u.username LIKE %s")
|
||||||
params.append("%" + search_username + "%")
|
params.append("%" + search_username + "%")
|
||||||
where_sql = " AND ".join(where_parts)
|
where_sql = " AND ".join(where_parts)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id,
|
"""SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id,
|
||||||
creator.username AS creator_username
|
creator.username AS creator_username
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN users creator ON creator.id = u.created_by_id
|
LEFT JOIN users creator ON creator.id = u.created_by_id
|
||||||
WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""",
|
WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""",
|
||||||
tuple(params) + (page_size, offset),
|
tuple(params) + (page_size, offset),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT COUNT(*) as total FROM users u WHERE " + where_sql,
|
"SELECT COUNT(*) as total FROM users u WHERE " + where_sql,
|
||||||
tuple(params),
|
tuple(params),
|
||||||
)
|
)
|
||||||
total = cur.fetchone()['total']
|
total = cur.fetchone()['total']
|
||||||
admins = []
|
admins = []
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
'id': r['id'],
|
'id': r['id'],
|
||||||
'username': r['username'],
|
'username': r['username'],
|
||||||
'is_admin': bool(r.get('is_admin')),
|
'is_admin': bool(r.get('is_admin')),
|
||||||
'role': r.get('role') or 'normal',
|
'role': r.get('role') or 'normal',
|
||||||
'created_by_id': r.get('created_by_id'),
|
'created_by_id': r.get('created_by_id'),
|
||||||
'creator_username': r.get('creator_username') or '',
|
'creator_username': r.get('creator_username') or '',
|
||||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||||
}
|
}
|
||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
conn.close()
|
conn.close()
|
||||||
payload = {
|
payload = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'items': items,
|
'items': items,
|
||||||
'total': total,
|
'total': total,
|
||||||
'page': page,
|
'page': page,
|
||||||
'page_size': page_size,
|
'page_size': page_size,
|
||||||
'current_user_role': role,
|
'current_user_role': role,
|
||||||
'admins': admins,
|
'admins': admins,
|
||||||
}
|
}
|
||||||
return jsonify(payload)
|
return jsonify(payload)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/api/admin/user', methods=['POST'])
|
@admin_bp.route('/api/admin/user', methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_create_user():
|
def admin_create_user():
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
username = (data.get('username') or '').strip()
|
username = (data.get('username') or '').strip()
|
||||||
password = data.get('password') or ''
|
password = data.get('password') or ''
|
||||||
role, current_row = _get_current_admin_role()
|
role, current_row = _get_current_admin_role()
|
||||||
if not role:
|
if not role:
|
||||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||||
want_role = (data.get('role') or 'normal').strip() or 'normal'
|
want_role = (data.get('role') or 'normal').strip() or 'normal'
|
||||||
if want_role not in ('admin', 'normal'):
|
if want_role not in ('admin', 'normal'):
|
||||||
want_role = 'normal'
|
want_role = 'normal'
|
||||||
if role == 'admin' and want_role == 'admin':
|
if role == 'admin' and want_role == 'admin':
|
||||||
return jsonify({'success': False, 'error': '仅超级管理员可创建管理员'})
|
return jsonify({'success': False, 'error': '仅超级管理员可创建管理员'})
|
||||||
if want_role == 'admin':
|
if want_role == 'admin':
|
||||||
want_created_by = current_row['id']
|
want_created_by = current_row['id']
|
||||||
elif role == 'super_admin':
|
elif role == 'super_admin':
|
||||||
want_created_by = data.get('created_by_id')
|
want_created_by = data.get('created_by_id')
|
||||||
else:
|
else:
|
||||||
want_created_by = current_row['id']
|
want_created_by = current_row['id']
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
return jsonify({'success': False, 'error': '用户名和密码不能为空'})
|
return jsonify({'success': False, 'error': '用户名和密码不能为空'})
|
||||||
if len(username) < 2:
|
if len(username) < 2:
|
||||||
return jsonify({'success': False, 'error': '用户名至少2个字符'})
|
return jsonify({'success': False, 'error': '用户名至少2个字符'})
|
||||||
if len(password) < 6:
|
if len(password) < 6:
|
||||||
return jsonify({'success': False, 'error': '密码至少6个字符'})
|
return jsonify({'success': False, 'error': '密码至少6个字符'})
|
||||||
if want_role == 'normal' and role == 'super_admin' and want_created_by is None:
|
if want_role == 'normal' and role == 'super_admin' and want_created_by is None:
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1")
|
cur.execute("SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1")
|
||||||
r = cur.fetchone()
|
r = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
want_created_by = r['id'] if r else current_row['id']
|
want_created_by = r['id'] if r else current_row['id']
|
||||||
except Exception:
|
except Exception:
|
||||||
want_created_by = current_row['id']
|
want_created_by = current_row['id']
|
||||||
if want_role == 'normal' and want_created_by is None:
|
if want_role == 'normal' and want_created_by is None:
|
||||||
want_created_by = current_row['id']
|
want_created_by = current_row['id']
|
||||||
is_admin = 1 if want_role in ('super_admin', 'admin') else 0
|
is_admin = 1 if want_role in ('super_admin', 'admin') else 0
|
||||||
pwd_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
pwd_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT INTO users (username, password_hash, is_admin, role, created_by_id) VALUES (%s, %s, %s, %s, %s)",
|
"INSERT INTO users (username, password_hash, is_admin, role, created_by_id) VALUES (%s, %s, %s, %s, %s)",
|
||||||
(username, pwd_hash, is_admin, want_role, want_created_by),
|
(username, pwd_hash, is_admin, want_role, want_created_by),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': True, 'msg': '用户创建成功'})
|
return jsonify({'success': True, 'msg': '用户创建成功'})
|
||||||
except pymysql.IntegrityError:
|
except pymysql.IntegrityError:
|
||||||
return jsonify({'success': False, 'error': '用户名已存在'})
|
return jsonify({'success': False, 'error': '用户名已存在'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/api/admin/user/<int:uid>', methods=['PUT'])
|
@admin_bp.route('/api/admin/user/<int:uid>', methods=['PUT'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_update_user(uid):
|
def admin_update_user(uid):
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
password = data.get('password')
|
password = data.get('password')
|
||||||
want_role = (data.get('role') or '').strip() or data.get('role')
|
want_role = (data.get('role') or '').strip() or data.get('role')
|
||||||
role, current_row = _get_current_admin_role()
|
role, current_row = _get_current_admin_role()
|
||||||
if not role:
|
if not role:
|
||||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||||
if want_role is None and not password:
|
if want_role is None and not password:
|
||||||
return jsonify({'success': False, 'error': '请提供要修改的内容'})
|
return jsonify({'success': False, 'error': '请提供要修改的内容'})
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,))
|
cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,))
|
||||||
target = cur.fetchone()
|
target = cur.fetchone()
|
||||||
if not target:
|
if not target:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': False, 'error': '用户不存在'})
|
return jsonify({'success': False, 'error': '用户不存在'})
|
||||||
if role == 'admin':
|
if role == 'admin':
|
||||||
if target['role'] != 'normal' or target.get('created_by_id') != current_row['id']:
|
if target['role'] != 'normal' or target.get('created_by_id') != current_row['id']:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': False, 'error': '只能编辑自己创建的普通用户'}), 403
|
return jsonify({'success': False, 'error': '只能编辑自己创建的普通用户'}), 403
|
||||||
want_role = None
|
want_role = None
|
||||||
else:
|
else:
|
||||||
if target.get('role') == 'super_admin':
|
if target.get('role') == 'super_admin':
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': False, 'error': '不能修改超级管理员'})
|
return jsonify({'success': False, 'error': '不能修改超级管理员'})
|
||||||
if want_role == 'super_admin':
|
if want_role == 'super_admin':
|
||||||
return jsonify({'success': False, 'error': '不能将用户设为超级管理员'})
|
return jsonify({'success': False, 'error': '不能将用户设为超级管理员'})
|
||||||
if want_role not in ('admin', 'normal', None, ''):
|
if want_role not in ('admin', 'normal', None, ''):
|
||||||
want_role = None
|
want_role = None
|
||||||
if password:
|
if password:
|
||||||
if len(password) < 6:
|
if len(password) < 6:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': False, 'error': '密码至少6个字符'})
|
return jsonify({'success': False, 'error': '密码至少6个字符'})
|
||||||
pwd_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
pwd_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
||||||
cur.execute("UPDATE users SET password_hash = %s WHERE id = %s", (pwd_hash, uid))
|
cur.execute("UPDATE users SET password_hash = %s WHERE id = %s", (pwd_hash, uid))
|
||||||
if want_role is not None and want_role != '':
|
if want_role is not None and want_role != '':
|
||||||
is_admin = 1 if want_role == 'admin' else 0
|
is_admin = 1 if want_role == 'admin' else 0
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"UPDATE users SET is_admin = %s, role = %s WHERE id = %s",
|
"UPDATE users SET is_admin = %s, role = %s WHERE id = %s",
|
||||||
(is_admin, want_role, uid),
|
(is_admin, want_role, uid),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': True, 'msg': '更新成功'})
|
return jsonify({'success': True, 'msg': '更新成功'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/api/admin/user/<int:uid>', methods=['DELETE'])
|
@admin_bp.route('/api/admin/user/<int:uid>', methods=['DELETE'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_delete_user(uid):
|
def admin_delete_user(uid):
|
||||||
from flask import session
|
from flask import session
|
||||||
if session.get('user_id') == uid:
|
if session.get('user_id') == uid:
|
||||||
return jsonify({'success': False, 'error': '不能删除当前登录账号'})
|
return jsonify({'success': False, 'error': '不能删除当前登录账号'})
|
||||||
role, current_row = _get_current_admin_role()
|
role, current_row = _get_current_admin_role()
|
||||||
if not role:
|
if not role:
|
||||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,))
|
cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,))
|
||||||
target = cur.fetchone()
|
target = cur.fetchone()
|
||||||
if not target:
|
if not target:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': False, 'error': '用户不存在'})
|
return jsonify({'success': False, 'error': '用户不存在'})
|
||||||
if target.get('role') == 'super_admin':
|
if target.get('role') == 'super_admin':
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': False, 'error': '不能删除超级管理员'})
|
return jsonify({'success': False, 'error': '不能删除超级管理员'})
|
||||||
if role == 'admin':
|
if role == 'admin':
|
||||||
if target.get('role') != 'normal' or target.get('created_by_id') != current_row['id']:
|
if target.get('role') != 'normal' or target.get('created_by_id') != current_row['id']:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'success': False, 'error': '只能删除自己创建的普通用户'}), 403
|
return jsonify({'success': False, 'error': '只能删除自己创建的普通用户'}), 403
|
||||||
cur.execute("DELETE FROM users WHERE id = %s", (uid,))
|
cur.execute("DELETE FROM users WHERE id = %s", (uid,))
|
||||||
affected = cur.rowcount
|
affected = cur.rowcount
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
if affected == 0:
|
if affected == 0:
|
||||||
return jsonify({'success': False, 'error': '用户不存在'})
|
return jsonify({'success': False, 'error': '用户不存在'})
|
||||||
return jsonify({'success': True, 'msg': '删除成功'})
|
return jsonify({'success': True, 'msg': '删除成功'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/api/admin/user/<int:uid>/column-permissions')
|
@admin_bp.route('/api/admin/user/<int:uid>/column-permissions')
|
||||||
@login_required
|
@login_required
|
||||||
def admin_user_column_permissions(uid):
|
def admin_user_column_permissions(uid):
|
||||||
"""获取指定用户的栏目权限列表:当前用户只能查自己,管理员可查任意用户。超级管理员返回全部栏目。"""
|
"""获取指定用户的栏目权限列表:当前用户只能查自己,管理员可查任意用户。超级管理员返回全部栏目。"""
|
||||||
current_uid = session.get('user_id')
|
current_uid = session.get('user_id')
|
||||||
if current_uid != uid:
|
if current_uid != uid:
|
||||||
role, _ = _get_current_admin_role()
|
role, _ = _get_current_admin_role()
|
||||||
if not role:
|
if not role:
|
||||||
return jsonify({'success': False, 'error': '无权查看该用户的栏目权限'}), 403
|
return jsonify({'success': False, 'error': '无权查看该用户的栏目权限'}), 403
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT role FROM users WHERE id = %s", (uid,))
|
cur.execute("SELECT role FROM users WHERE id = %s", (uid,))
|
||||||
user_row = cur.fetchone()
|
user_row = cur.fetchone()
|
||||||
if user_row and (user_row.get('role') or '').strip() == 'super_admin':
|
if user_row and (user_row.get('role') or '').strip() == 'super_admin':
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id, name, column_key, created_at FROM columns ORDER BY id
|
SELECT id, name, column_key, created_at FROM columns ORDER BY id
|
||||||
""")
|
""")
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
else:
|
else:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT c.id, c.name, c.column_key, c.created_at
|
SELECT c.id, c.name, c.column_key, c.created_at
|
||||||
FROM columns c
|
FROM columns c
|
||||||
INNER JOIN user_column_permission ucp ON ucp.column_id = c.id
|
INNER JOIN user_column_permission ucp ON ucp.column_id = c.id
|
||||||
WHERE ucp.user_id = %s
|
WHERE ucp.user_id = %s
|
||||||
ORDER BY c.id
|
ORDER BY c.id
|
||||||
""", (uid,))
|
""", (uid,))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
'id': r['id'],
|
'id': r['id'],
|
||||||
'name': r['name'],
|
'name': r['name'],
|
||||||
'column_key': r['column_key'],
|
'column_key': r['column_key'],
|
||||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||||
}
|
}
|
||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
return jsonify({'success': True, 'items': items})
|
return jsonify({'success': True, 'items': items})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/api/admin/history')
|
@admin_bp.route('/api/admin/history')
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_history():
|
def admin_history():
|
||||||
"""管理员分页获取所有生成记录,支持按用户和时间筛选"""
|
"""管理员分页获取所有生成记录,支持按用户和时间筛选"""
|
||||||
page = max(1, int(request.args.get('page', 1)))
|
page = max(1, int(request.args.get('page', 1)))
|
||||||
page_size = min(50, max(10, int(request.args.get('page_size', 15))))
|
page_size = min(50, max(10, int(request.args.get('page_size', 15))))
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
user_id = request.args.get('user_id', type=int)
|
user_id = request.args.get('user_id', type=int)
|
||||||
time_start = (request.args.get('time_start') or '').strip()
|
time_start = (request.args.get('time_start') or '').strip()
|
||||||
time_end = (request.args.get('time_end') or '').strip()
|
time_end = (request.args.get('time_end') or '').strip()
|
||||||
conditions, params = [], []
|
conditions, params = [], []
|
||||||
if user_id:
|
if user_id:
|
||||||
conditions.append("h.user_id = %s")
|
conditions.append("h.user_id = %s")
|
||||||
params.append(user_id)
|
params.append(user_id)
|
||||||
if time_start:
|
if time_start:
|
||||||
conditions.append("h.created_at >= %s")
|
conditions.append("h.created_at >= %s")
|
||||||
params.append(time_start)
|
params.append(time_start)
|
||||||
if time_end:
|
if time_end:
|
||||||
conditions.append("h.created_at <= %s")
|
conditions.append("h.created_at <= %s")
|
||||||
params.append(time_end + ' 23:59:59' if len(time_end) <= 10 else time_end)
|
params.append(time_end + ' 23:59:59' if len(time_end) <= 10 else time_end)
|
||||||
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
||||||
params_count = params[:]
|
params_count = params[:]
|
||||||
params.extend([page_size, offset])
|
params.extend([page_size, offset])
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT h.id, h.user_id, h.created_at, h.panel_type, h.original_urls, h.params, h.result_urls,
|
"""SELECT h.id, h.user_id, h.created_at, h.panel_type, h.original_urls, h.params, h.result_urls,
|
||||||
h.long_image_url, u.username
|
h.long_image_url, u.username
|
||||||
FROM image_history h
|
FROM image_history h
|
||||||
LEFT JOIN users u ON h.user_id = u.id
|
LEFT JOIN users u ON h.user_id = u.id
|
||||||
WHERE """ + where_clause + """ ORDER BY h.created_at DESC LIMIT %s OFFSET %s""",
|
WHERE """ + where_clause + """ ORDER BY h.created_at DESC LIMIT %s OFFSET %s""",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.execute("SELECT COUNT(*) as total FROM image_history h WHERE " + where_clause, params_count)
|
cur.execute("SELECT COUNT(*) as total FROM image_history h WHERE " + where_clause, params_count)
|
||||||
total = cur.fetchone()['total']
|
total = cur.fetchone()['total']
|
||||||
conn.close()
|
conn.close()
|
||||||
items = []
|
items = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
items.append({
|
items.append({
|
||||||
'id': r['id'],
|
'id': r['id'],
|
||||||
'user_id': r['user_id'],
|
'user_id': r['user_id'],
|
||||||
'username': r.get('username') or '-',
|
'username': r.get('username') or '-',
|
||||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else '',
|
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else '',
|
||||||
'panel_type': r['panel_type'] or '',
|
'panel_type': r['panel_type'] or '',
|
||||||
'original_urls': _parse_json(r['original_urls'], []),
|
'original_urls': _parse_json(r['original_urls'], []),
|
||||||
'params': _parse_json(r['params'], {}),
|
'params': _parse_json(r['params'], {}),
|
||||||
'result_urls': _parse_json(r['result_urls'], []),
|
'result_urls': _parse_json(r['result_urls'], []),
|
||||||
'long_image_url': (r.get('long_image_url') or '').strip() or None,
|
'long_image_url': (r.get('long_image_url') or '').strip() or None,
|
||||||
})
|
})
|
||||||
return jsonify({'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size})
|
return jsonify({'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
@@ -1,107 +1,107 @@
|
|||||||
"""
|
"""
|
||||||
认证蓝图:登录、登出、登录状态校验
|
认证蓝图:登录、登出、登录状态校验
|
||||||
"""
|
"""
|
||||||
from flask import Blueprint, request, redirect, url_for, session, jsonify
|
from flask import Blueprint, request, redirect, url_for, session, jsonify
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from app_common import (
|
from app_common import (
|
||||||
get_db,
|
get_db,
|
||||||
_render_html,
|
_render_html,
|
||||||
_is_session_user_valid,
|
_is_session_user_valid,
|
||||||
login_required,
|
login_required,
|
||||||
BASE_DIR,
|
BASE_DIR,
|
||||||
)
|
)
|
||||||
from tool.devices import DeviceIDGenerator
|
from tool.devices import DeviceIDGenerator
|
||||||
|
|
||||||
auth_bp = Blueprint('auth', __name__)
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if session.get('user_id') and _is_session_user_valid():
|
if session.get('user_id') and _is_session_user_valid():
|
||||||
return redirect(url_for('main.home'))
|
return redirect(url_for('main.home'))
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
data = request.get_json() if request.is_json else request.form
|
data = request.get_json() if request.is_json else request.form
|
||||||
username = (data.get('username') or '').strip()
|
username = (data.get('username') or '').strip()
|
||||||
password = data.get('password') or ''
|
password = data.get('password') or ''
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
return jsonify({'success': False, 'error': '请输入用户名和密码'})
|
return jsonify({'success': False, 'error': '请输入用户名和密码'})
|
||||||
return _render_html('login.html', error='请输入用户名和密码')
|
return _render_html('login.html', error='请输入用户名和密码')
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT id, password_hash, machine, is_admin FROM users WHERE username = %s",
|
"SELECT id, password_hash, machine, is_admin FROM users WHERE username = %s",
|
||||||
(username,)
|
(username,)
|
||||||
)
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if row and check_password_hash(row['password_hash'], password):
|
if row and check_password_hash(row['password_hash'], password):
|
||||||
current_machine = DeviceIDGenerator().get_device_id()
|
current_machine = DeviceIDGenerator().get_device_id()
|
||||||
stored_machine = (row.get('machine') or '').strip()
|
stored_machine = (row.get('machine') or '').strip()
|
||||||
if not stored_machine:
|
if not stored_machine:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("UPDATE users SET machine = %s WHERE id = %s", (current_machine, row['id']))
|
cur.execute("UPDATE users SET machine = %s WHERE id = %s", (current_machine, row['id']))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
session['user_id'] = row['id']
|
session['user_id'] = row['id']
|
||||||
session['username'] = username
|
session['username'] = username
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
return jsonify({'success': True, 'redirect': url_for('main.home')})
|
return jsonify({'success': True, 'redirect': url_for('main.home')})
|
||||||
return redirect(url_for('main.home'))
|
return redirect(url_for('main.home'))
|
||||||
print("验证设备",stored_machine)
|
print("验证设备",stored_machine)
|
||||||
print("当前设备",current_machine)
|
print("当前设备",current_machine)
|
||||||
if stored_machine != current_machine and row.get("is_admin") != 1:
|
if stored_machine != current_machine and row.get("is_admin") != 1:
|
||||||
conn.close()
|
conn.close()
|
||||||
err_msg = '当前设备与首次登录设备不一致,请在原设备上登录'
|
err_msg = '当前设备与首次登录设备不一致,请在原设备上登录'
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
return jsonify({'success': False, 'error': err_msg})
|
return jsonify({'success': False, 'error': err_msg})
|
||||||
return _render_html('login.html', error=err_msg)
|
return _render_html('login.html', error=err_msg)
|
||||||
conn.close()
|
conn.close()
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
session['user_id'] = row['id']
|
session['user_id'] = row['id']
|
||||||
session['username'] = username
|
session['username'] = username
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
return jsonify({'success': True, 'redirect': url_for('main.home')})
|
return jsonify({'success': True, 'redirect': url_for('main.home')})
|
||||||
return redirect(url_for('main.home'))
|
return redirect(url_for('main.home'))
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
return _render_html('login.html', error='登录失败,请稍后重试')
|
return _render_html('login.html', error='登录失败,请稍后重试')
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
return jsonify({'success': False, 'error': '用户名或密码错误'})
|
return jsonify({'success': False, 'error': '用户名或密码错误'})
|
||||||
return _render_html('login.html', error='用户名或密码错误')
|
return _render_html('login.html', error='用户名或密码错误')
|
||||||
return _render_html('login.html')
|
return _render_html('login.html')
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/api/auth/check')
|
@auth_bp.route('/api/auth/check')
|
||||||
@login_required
|
@login_required
|
||||||
def api_auth_check():
|
def api_auth_check():
|
||||||
"""校验登录状态,用于页面加载时判断是否已登录;同时校验机器码是否与首次登录设备一致"""
|
"""校验登录状态,用于页面加载时判断是否已登录;同时校验机器码是否与首次登录设备一致"""
|
||||||
if not session.get('user_id'):
|
if not session.get('user_id'):
|
||||||
return jsonify({'logged_in': False})
|
return jsonify({'logged_in': False})
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT machine, is_admin FROM users WHERE id = %s", (session['user_id'],))
|
cur.execute("SELECT machine, is_admin FROM users WHERE id = %s", (session['user_id'],))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not row:
|
if not row:
|
||||||
return jsonify({'logged_in': False})
|
return jsonify({'logged_in': False})
|
||||||
stored_machine = (row.get('machine') or '').strip()
|
stored_machine = (row.get('machine') or '').strip()
|
||||||
if stored_machine:
|
if stored_machine:
|
||||||
current_machine = DeviceIDGenerator().get_device_id()
|
current_machine = DeviceIDGenerator().get_device_id()
|
||||||
if stored_machine != current_machine and row.get("is_admin") != 1:
|
if stored_machine != current_machine and row.get("is_admin") != 1:
|
||||||
session.clear()
|
session.clear()
|
||||||
return jsonify({'logged_in': False, 'error': '当前设备与首次登录设备不一致'})
|
return jsonify({'logged_in': False, 'error': '当前设备与首次登录设备不一致'})
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({'logged_in': False})
|
return jsonify({'logged_in': False})
|
||||||
return jsonify({'logged_in': True, 'redirect': url_for('main.home')})
|
return jsonify({'logged_in': True, 'redirect': url_for('main.home')})
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/logout')
|
@auth_bp.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
session.clear()
|
session.clear()
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
@@ -1,411 +1,411 @@
|
|||||||
"""
|
"""
|
||||||
图片生成蓝图:生成、历史、下载、拼接、版本
|
图片生成蓝图:生成、历史、下载、拼接、版本
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import subprocess
|
import subprocess
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import urlparse, quote
|
from urllib.parse import urlparse, quote
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify, Response, session, send_file
|
from flask import Blueprint, request, jsonify, Response, session, send_file
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from app_common import get_db, login_required, BASE_DIR
|
from app_common import get_db, login_required, BASE_DIR
|
||||||
from config import STITCH_WORKFLOW_ID,client_name
|
from config import STITCH_WORKFLOW_ID,client_name
|
||||||
|
|
||||||
image_bp = Blueprint('image', __name__)
|
image_bp = Blueprint('image', __name__)
|
||||||
|
|
||||||
|
|
||||||
def _load_image_from_url_or_data(url_or_data):
|
def _load_image_from_url_or_data(url_or_data):
|
||||||
"""从 http(s) URL 或 data URL 加载为 PIL Image,失败返回 None"""
|
"""从 http(s) URL 或 data URL 加载为 PIL Image,失败返回 None"""
|
||||||
if not url_or_data or not isinstance(url_or_data, str):
|
if not url_or_data or not isinstance(url_or_data, str):
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
if url_or_data.startswith('data:'):
|
if url_or_data.startswith('data:'):
|
||||||
m = re.match(r'data:image/[^;]+;base64,(.+)', url_or_data, re.DOTALL)
|
m = re.match(r'data:image/[^;]+;base64,(.+)', url_or_data, re.DOTALL)
|
||||||
if not m:
|
if not m:
|
||||||
return None
|
return None
|
||||||
raw = base64.b64decode(m.group(1).strip())
|
raw = base64.b64decode(m.group(1).strip())
|
||||||
img = Image.open(io.BytesIO(raw))
|
img = Image.open(io.BytesIO(raw))
|
||||||
elif url_or_data.startswith(('http://', 'https://')):
|
elif url_or_data.startswith(('http://', 'https://')):
|
||||||
resp = requests.get(url_or_data, timeout=15)
|
resp = requests.get(url_or_data, timeout=15)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
img = Image.open(io.BytesIO(resp.content))
|
img = Image.open(io.BytesIO(resp.content))
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
if img.mode != 'RGB':
|
if img.mode != 'RGB':
|
||||||
img = img.convert('RGB')
|
img = img.convert('RGB')
|
||||||
return img
|
return img
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _stitch_and_upload_long_image(urls):
|
def _stitch_and_upload_long_image(urls):
|
||||||
"""将多张图片 URL 先上传获取 file_id,再调用 workflow_run 拼接长图,返回 data.merged_image_url;失败返回 None。"""
|
"""将多张图片 URL 先上传获取 file_id,再调用 workflow_run 拼接长图,返回 data.merged_image_url;失败返回 None。"""
|
||||||
if not urls or not isinstance(urls, (list, tuple)):
|
if not urls or not isinstance(urls, (list, tuple)):
|
||||||
return None
|
return None
|
||||||
from coze import upload_file as coze_upload_file, workflow_run
|
from coze import upload_file as coze_upload_file, workflow_run
|
||||||
file_ids = []
|
file_ids = []
|
||||||
temp_paths = []
|
temp_paths = []
|
||||||
try:
|
try:
|
||||||
for u in urls:
|
for u in urls:
|
||||||
img = _load_image_from_url_or_data(u)
|
img = _load_image_from_url_or_data(u)
|
||||||
if img is None:
|
if img is None:
|
||||||
continue
|
continue
|
||||||
fd, path = tempfile.mkstemp(suffix='.png')
|
fd, path = tempfile.mkstemp(suffix='.png')
|
||||||
try:
|
try:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
img.save(path)
|
img.save(path)
|
||||||
temp_paths.append(path)
|
temp_paths.append(path)
|
||||||
resp = coze_upload_file(path)
|
resp = coze_upload_file(path)
|
||||||
if resp.get('code') == 0 and resp.get('data', {}).get('id'):
|
if resp.get('code') == 0 and resp.get('data', {}).get('id'):
|
||||||
file_ids.append(resp['data']['id'])
|
file_ids.append(resp['data']['id'])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if not file_ids:
|
if not file_ids:
|
||||||
return None
|
return None
|
||||||
parameters = {"images": [{"file_id": fid} for fid in file_ids]}
|
parameters = {"images": [{"file_id": fid} for fid in file_ids]}
|
||||||
resp = workflow_run(STITCH_WORKFLOW_ID, parameters, is_async=False)
|
resp = workflow_run(STITCH_WORKFLOW_ID, parameters, is_async=False)
|
||||||
if resp.get('code') != 0:
|
if resp.get('code') != 0:
|
||||||
return None
|
return None
|
||||||
data = resp.get('data') or {}
|
data = resp.get('data') or {}
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
merged_image_url = data.get('merged_image_url')
|
merged_image_url = data.get('merged_image_url')
|
||||||
if merged_image_url:
|
if merged_image_url:
|
||||||
return merged_image_url
|
return merged_image_url
|
||||||
output_str = data.get('output') or ''
|
output_str = data.get('output') or ''
|
||||||
if output_str:
|
if output_str:
|
||||||
try:
|
try:
|
||||||
outer = json.loads(output_str)
|
outer = json.loads(output_str)
|
||||||
inner_str = outer.get('Output', '{}')
|
inner_str = outer.get('Output', '{}')
|
||||||
inner = json.loads(inner_str)
|
inner = json.loads(inner_str)
|
||||||
data_str = inner.get('data', '[]')
|
data_str = inner.get('data', '[]')
|
||||||
inner_data = json.loads(data_str)
|
inner_data = json.loads(data_str)
|
||||||
merged_image_url = inner_data.get('merged_image_url')
|
merged_image_url = inner_data.get('merged_image_url')
|
||||||
return merged_image_url
|
return merged_image_url
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
for p in temp_paths:
|
for p in temp_paths:
|
||||||
try:
|
try:
|
||||||
os.unlink(p)
|
os.unlink(p)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_params_for_history(params):
|
def _sanitize_params_for_history(params):
|
||||||
"""移除 base64 大字段及敏感字段,仅保留可存储的请求参数"""
|
"""移除 base64 大字段及敏感字段,仅保留可存储的请求参数"""
|
||||||
exclude = ('ref_images', 'proc_images', 'layout_image')
|
exclude = ('ref_images', 'proc_images', 'layout_image')
|
||||||
out = {}
|
out = {}
|
||||||
for k, v in (params or {}).items():
|
for k, v in (params or {}).items():
|
||||||
if k == 'api_key':
|
if k == 'api_key':
|
||||||
continue
|
continue
|
||||||
if k in exclude:
|
if k in exclude:
|
||||||
if isinstance(v, list):
|
if isinstance(v, list):
|
||||||
out[f'{k}_count'] = len(v)
|
out[f'{k}_count'] = len(v)
|
||||||
else:
|
else:
|
||||||
out[f'{k}_count'] = 1 if v else 0
|
out[f'{k}_count'] = 1 if v else 0
|
||||||
elif isinstance(v, (str, int, float, bool, type(None))):
|
elif isinstance(v, (str, int, float, bool, type(None))):
|
||||||
out[k] = v
|
out[k] = v
|
||||||
elif isinstance(v, list) and not v:
|
elif isinstance(v, list) and not v:
|
||||||
out[k] = []
|
out[k] = []
|
||||||
elif isinstance(v, list) and isinstance(v[0], (str, int, float, bool)):
|
elif isinstance(v, list) and isinstance(v[0], (str, int, float, bool)):
|
||||||
out[k] = v
|
out[k] = v
|
||||||
else:
|
else:
|
||||||
out[k] = str(v)[:200] if v else None
|
out[k] = str(v)[:200] if v else None
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@image_bp.route('/api/generate', methods=['POST'])
|
@image_bp.route('/api/generate', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def api_generate():
|
def api_generate():
|
||||||
"""生成图片:调用 generate_api,上传原图到 OSS,保存历史记录"""
|
"""生成图片:调用 generate_api,上传原图到 OSS,保存历史记录"""
|
||||||
try:
|
try:
|
||||||
params = request.get_json() or {}
|
params = request.get_json() or {}
|
||||||
from generate_api import generate
|
from generate_api import generate
|
||||||
result = generate(params)
|
result = generate(params)
|
||||||
if result.get('success') and result.get('urls'):
|
if result.get('success') and result.get('urls'):
|
||||||
long_image_url = result.get("long_image_url")
|
long_image_url = result.get("long_image_url")
|
||||||
result["long_image_url"] = long_image_url
|
result["long_image_url"] = long_image_url
|
||||||
import json as _json
|
import json as _json
|
||||||
history_id = None
|
history_id = None
|
||||||
try:
|
try:
|
||||||
hid = params.get('history_id')
|
hid = params.get('history_id')
|
||||||
if hid is not None:
|
if hid is not None:
|
||||||
try:
|
try:
|
||||||
hid = int(hid)
|
hid = int(hid)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
hid = None
|
hid = None
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
if hid is not None and hid > 0:
|
if hid is not None and hid > 0:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT result_urls FROM image_history WHERE id=%s AND user_id=%s",
|
"SELECT result_urls FROM image_history WHERE id=%s AND user_id=%s",
|
||||||
(hid, session['user_id']),
|
(hid, session['user_id']),
|
||||||
)
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
existing_urls = []
|
existing_urls = []
|
||||||
if row and row.get('result_urls'):
|
if row and row.get('result_urls'):
|
||||||
try:
|
try:
|
||||||
existing_urls = _json.loads(row['result_urls'])
|
existing_urls = _json.loads(row['result_urls'])
|
||||||
except Exception:
|
except Exception:
|
||||||
existing_urls = []
|
existing_urls = []
|
||||||
new_urls = result.get('urls') or []
|
new_urls = result.get('urls') or []
|
||||||
new_url = new_urls[0] if new_urls else None
|
new_url = new_urls[0] if new_urls else None
|
||||||
idx = params.get('history_index', 0)
|
idx = params.get('history_index', 0)
|
||||||
try:
|
try:
|
||||||
idx = int(idx)
|
idx = int(idx)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
idx = 0
|
idx = 0
|
||||||
if new_url:
|
if new_url:
|
||||||
if not isinstance(existing_urls, list):
|
if not isinstance(existing_urls, list):
|
||||||
existing_urls = []
|
existing_urls = []
|
||||||
while len(existing_urls) <= idx:
|
while len(existing_urls) <= idx:
|
||||||
existing_urls.append(existing_urls[-1] if existing_urls else new_url)
|
existing_urls.append(existing_urls[-1] if existing_urls else new_url)
|
||||||
existing_urls[idx] = new_url
|
existing_urls[idx] = new_url
|
||||||
merged_result_urls = existing_urls or new_urls
|
merged_result_urls = existing_urls or new_urls
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""UPDATE image_history SET panel_type=%s, original_urls=%s, params=%s, result_urls=%s
|
"""UPDATE image_history SET panel_type=%s, original_urls=%s, params=%s, result_urls=%s
|
||||||
WHERE id=%s AND user_id=%s""",
|
WHERE id=%s AND user_id=%s""",
|
||||||
(
|
(
|
||||||
params.get('panel_type', ''),
|
params.get('panel_type', ''),
|
||||||
_json.dumps(result.get('original_urls') or []),
|
_json.dumps(result.get('original_urls') or []),
|
||||||
_json.dumps(_sanitize_params_for_history(params)),
|
_json.dumps(_sanitize_params_for_history(params)),
|
||||||
_json.dumps(merged_result_urls),
|
_json.dumps(merged_result_urls),
|
||||||
hid,
|
hid,
|
||||||
session['user_id'],
|
session['user_id'],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if cur.rowcount > 0:
|
if cur.rowcount > 0:
|
||||||
history_id = hid
|
history_id = hid
|
||||||
else:
|
else:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""INSERT INTO image_history (user_id, panel_type, original_urls, params, result_urls, long_image_url)
|
"""INSERT INTO image_history (user_id, panel_type, original_urls, params, result_urls, long_image_url)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s)""",
|
VALUES (%s, %s, %s, %s, %s, %s)""",
|
||||||
(
|
(
|
||||||
session['user_id'],
|
session['user_id'],
|
||||||
params.get('panel_type', ''),
|
params.get('panel_type', ''),
|
||||||
_json.dumps(result.get('original_urls') or []),
|
_json.dumps(result.get('original_urls') or []),
|
||||||
_json.dumps(_sanitize_params_for_history(params)),
|
_json.dumps(_sanitize_params_for_history(params)),
|
||||||
_json.dumps(result.get('urls') or []),
|
_json.dumps(result.get('urls') or []),
|
||||||
long_image_url,
|
long_image_url,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
history_id = cur.lastrowid
|
history_id = cur.lastrowid
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if history_id is not None:
|
if history_id is not None:
|
||||||
result['history_id'] = history_id
|
result['history_id'] = history_id
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'urls': [], 'error': str(e)})
|
return jsonify({'success': False, 'urls': [], 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@image_bp.route('/api/version')
|
@image_bp.route('/api/version')
|
||||||
def api_version():
|
def api_version():
|
||||||
"""检测更新:请求 APP_UPDATE_URL 获取最新版本信息,与当前版本比较"""
|
"""检测更新:请求 APP_UPDATE_URL 获取最新版本信息,与当前版本比较"""
|
||||||
current_version = (os.environ.get('APP_VERSION', '1.0.0') or '1.0.0').strip()
|
current_version = (os.environ.get('APP_VERSION', '1.0.0') or '1.0.0').strip()
|
||||||
update_url = (os.environ.get('APP_UPDATE_URL', '') or '').strip()
|
update_url = (os.environ.get('APP_UPDATE_URL', '') or '').strip()
|
||||||
result = {
|
result = {
|
||||||
'version': current_version,
|
'version': current_version,
|
||||||
'desc': '',
|
'desc': '',
|
||||||
'url': '',
|
'url': '',
|
||||||
'has_update': False,
|
'has_update': False,
|
||||||
'latest_version': current_version,
|
'latest_version': current_version,
|
||||||
'file_url': '',
|
'file_url': '',
|
||||||
}
|
}
|
||||||
if not update_url:
|
if not update_url:
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
try:
|
try:
|
||||||
resp = requests.get(update_url, timeout=10)
|
resp = requests.get(update_url, timeout=10)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json() or {}
|
data = resp.json() or {}
|
||||||
latest_version = (data.get('version') or '').strip()
|
latest_version = (data.get('version') or '').strip()
|
||||||
file_url = (data.get('file_url') or '').strip()
|
file_url = (data.get('file_url') or '').strip()
|
||||||
result['latest_version'] = latest_version
|
result['latest_version'] = latest_version
|
||||||
result['file_url'] = file_url
|
result['file_url'] = file_url
|
||||||
result['url'] = file_url
|
result['url'] = file_url
|
||||||
# 版本不一致则视为有更新
|
# 版本不一致则视为有更新
|
||||||
if latest_version and latest_version != current_version:
|
if latest_version and latest_version != current_version:
|
||||||
result['has_update'] = True
|
result['has_update'] = True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
def _run_update_and_exit(zip_path, target_dir):
|
def _run_update_and_exit(zip_path, target_dir):
|
||||||
"""在后台延迟后启动 update.exe(脱离当前进程),然后退出当前程序"""
|
"""在后台延迟后启动 update.exe(脱离当前进程),然后退出当前程序"""
|
||||||
def _do():
|
def _do():
|
||||||
import time
|
import time
|
||||||
time.sleep(1.5) # 确保 HTTP 响应已发送
|
time.sleep(1.5) # 确保 HTTP 响应已发送
|
||||||
# exe_dir = target_dir
|
# exe_dir = target_dir
|
||||||
# exe_dir = "D:\\pack\\nanri\\update_exe\\main.dist"
|
# exe_dir = "D:\\pack\\nanri\\update_exe\\main.dist"
|
||||||
exe_dir = os.path.join(BASE_DIR,"update")
|
exe_dir = os.path.join(BASE_DIR,"update")
|
||||||
update_exe = os.path.join(exe_dir, 'update.exe')
|
update_exe = os.path.join(exe_dir, 'update.exe')
|
||||||
if not os.path.isfile(update_exe):
|
if not os.path.isfile(update_exe):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
creationflags = 0
|
creationflags = 0
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
[update_exe, '--zip', zip_path, '--target', target_dir, '--process',client_name],
|
[update_exe, '--zip', zip_path, '--target', target_dir, '--process',client_name],
|
||||||
cwd=exe_dir,
|
cwd=exe_dir,
|
||||||
creationflags=creationflags,
|
creationflags=creationflags,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
t = threading.Thread(target=_do, daemon=False)
|
t = threading.Thread(target=_do, daemon=False)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
@image_bp.route('/api/update/do', methods=['POST'])
|
@image_bp.route('/api/update/do', methods=['POST'])
|
||||||
def api_update_do():
|
def api_update_do():
|
||||||
"""执行更新:下载 zip 到 tmp,启动 update.exe 后退出程序"""
|
"""执行更新:下载 zip 到 tmp,启动 update.exe 后退出程序"""
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
file_url = (data.get('file_url') or '').strip()
|
file_url = (data.get('file_url') or '').strip()
|
||||||
if not file_url:
|
if not file_url:
|
||||||
return jsonify({'success': False, 'error': '缺少 file_url'}), 400
|
return jsonify({'success': False, 'error': '缺少 file_url'}), 400
|
||||||
parsed = urlparse(file_url)
|
parsed = urlparse(file_url)
|
||||||
if parsed.scheme not in ('http', 'https'):
|
if parsed.scheme not in ('http', 'https'):
|
||||||
return jsonify({'success': False, 'error': '无效的下载地址'}), 400
|
return jsonify({'success': False, 'error': '无效的下载地址'}), 400
|
||||||
tmp_dir = os.path.join(BASE_DIR, 'tmp')
|
tmp_dir = os.path.join(BASE_DIR, 'tmp')
|
||||||
try:
|
try:
|
||||||
os.makedirs(tmp_dir, exist_ok=True)
|
os.makedirs(tmp_dir, exist_ok=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': f'创建 tmp 目录失败: {e}'}), 500
|
return jsonify({'success': False, 'error': f'创建 tmp 目录失败: {e}'}), 500
|
||||||
# 使用 URL 中的文件名或默认版本名
|
# 使用 URL 中的文件名或默认版本名
|
||||||
filename = os.path.basename(parsed.path) or 'update.zip'
|
filename = os.path.basename(parsed.path) or 'update.zip'
|
||||||
zip_path = os.path.join(tmp_dir, filename)
|
zip_path = os.path.join(tmp_dir, filename)
|
||||||
try:
|
try:
|
||||||
resp = requests.get(file_url, timeout=300, stream=True)
|
resp = requests.get(file_url, timeout=300, stream=True)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
with open(zip_path, 'wb') as f:
|
with open(zip_path, 'wb') as f:
|
||||||
for chunk in resp.iter_content(chunk_size=65536):
|
for chunk in resp.iter_content(chunk_size=65536):
|
||||||
if chunk:
|
if chunk:
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
return jsonify({'success': False, 'error': f'下载失败: {e}'}), 502
|
return jsonify({'success': False, 'error': f'下载失败: {e}'}), 502
|
||||||
_run_update_and_exit(zip_path, BASE_DIR)
|
_run_update_and_exit(zip_path, BASE_DIR)
|
||||||
return jsonify({'success': True, 'message': '更新已启动,程序即将退出'})
|
return jsonify({'success': True, 'message': '更新已启动,程序即将退出'})
|
||||||
|
|
||||||
|
|
||||||
@image_bp.route('/api/download')
|
@image_bp.route('/api/download')
|
||||||
@login_required
|
@login_required
|
||||||
def api_download():
|
def api_download():
|
||||||
"""代理下载图片,解决跨域 fetch 无法下载的问题"""
|
"""代理下载图片,解决跨域 fetch 无法下载的问题"""
|
||||||
url = request.args.get('url', '').strip()
|
url = request.args.get('url', '').strip()
|
||||||
filename = request.args.get('filename', 'image.png')
|
filename = request.args.get('filename', 'image.png')
|
||||||
if not url:
|
if not url:
|
||||||
return jsonify({'success': False, 'error': '缺少 url 参数'}), 400
|
return jsonify({'success': False, 'error': '缺少 url 参数'}), 400
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
if parsed.scheme not in ('http', 'https'):
|
if parsed.scheme not in ('http', 'https'):
|
||||||
return jsonify({'success': False, 'error': '仅支持 http/https 链接'}), 400
|
return jsonify({'success': False, 'error': '仅支持 http/https 链接'}), 400
|
||||||
try:
|
try:
|
||||||
resp = requests.get(url, timeout=30, stream=True)
|
resp = requests.get(url, timeout=30, stream=True)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
content_type = resp.headers.get('Content-Type', 'image/png')
|
content_type = resp.headers.get('Content-Type', 'image/png')
|
||||||
encoded = quote(filename, safe='')
|
encoded = quote(filename, safe='')
|
||||||
disposition = f"attachment; filename*=UTF-8''{encoded}"
|
disposition = f"attachment; filename*=UTF-8''{encoded}"
|
||||||
return Response(
|
return Response(
|
||||||
resp.iter_content(chunk_size=8192),
|
resp.iter_content(chunk_size=8192),
|
||||||
mimetype=content_type,
|
mimetype=content_type,
|
||||||
headers={'Content-Disposition': disposition}
|
headers={'Content-Disposition': disposition}
|
||||||
)
|
)
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 502
|
return jsonify({'success': False, 'error': str(e)}), 502
|
||||||
|
|
||||||
|
|
||||||
@image_bp.route('/api/stitch/save', methods=['POST'])
|
@image_bp.route('/api/stitch/save', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def api_stitch_save():
|
def api_stitch_save():
|
||||||
"""手动拼接:接收图片 URL 列表(支持 http 或 data URL),拼接并上传,返回长图 URL"""
|
"""手动拼接:接收图片 URL 列表(支持 http 或 data URL),拼接并上传,返回长图 URL"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
urls = data.get('urls')
|
urls = data.get('urls')
|
||||||
if not urls or not isinstance(urls, list):
|
if not urls or not isinstance(urls, list):
|
||||||
return jsonify({'success': False, 'error': '请提供 urls 数组'}), 400
|
return jsonify({'success': False, 'error': '请提供 urls 数组'}), 400
|
||||||
urls = [u for u in urls if u and isinstance(u, str)]
|
urls = [u for u in urls if u and isinstance(u, str)]
|
||||||
if not urls:
|
if not urls:
|
||||||
return jsonify({'success': False, 'error': '没有有效的图片'}), 400
|
return jsonify({'success': False, 'error': '没有有效的图片'}), 400
|
||||||
long_image_url = _stitch_and_upload_long_image(urls)
|
long_image_url = _stitch_and_upload_long_image(urls)
|
||||||
if not long_image_url:
|
if not long_image_url:
|
||||||
return jsonify({'success': False, 'error': '拼接或上传失败'}), 500
|
return jsonify({'success': False, 'error': '拼接或上传失败'}), 500
|
||||||
return jsonify({'success': True, 'long_image_url': long_image_url})
|
return jsonify({'success': True, 'long_image_url': long_image_url})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@image_bp.route('/api/history')
|
@image_bp.route('/api/history')
|
||||||
@login_required
|
@login_required
|
||||||
def api_history():
|
def api_history():
|
||||||
"""分页获取当前用户的历史图库,支持按 panel_type 栏目筛选"""
|
"""分页获取当前用户的历史图库,支持按 panel_type 栏目筛选"""
|
||||||
page = max(1, int(request.args.get('page', 1)))
|
page = max(1, int(request.args.get('page', 1)))
|
||||||
page_size = min(50, max(10, int(request.args.get('page_size', 20))))
|
page_size = min(50, max(10, int(request.args.get('page_size', 20))))
|
||||||
panel_type = (request.args.get('panel_type') or '').strip()
|
panel_type = (request.args.get('panel_type') or '').strip()
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
where_user = "user_id = %s"
|
where_user = "user_id = %s"
|
||||||
params_where = [session['user_id']]
|
params_where = [session['user_id']]
|
||||||
if panel_type:
|
if panel_type:
|
||||||
where_user += " AND panel_type = %s"
|
where_user += " AND panel_type = %s"
|
||||||
params_where.append(panel_type)
|
params_where.append(panel_type)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT id, created_at, panel_type, original_urls, params, result_urls, long_image_url
|
"""SELECT id, created_at, panel_type, original_urls, params, result_urls, long_image_url
|
||||||
FROM image_history WHERE """ + where_user + """ ORDER BY created_at DESC LIMIT %s OFFSET %s""",
|
FROM image_history WHERE """ + where_user + """ ORDER BY created_at DESC LIMIT %s OFFSET %s""",
|
||||||
params_where + [page_size, offset],
|
params_where + [page_size, offset],
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.execute("SELECT COUNT(*) as total FROM image_history WHERE " + where_user, params_where)
|
cur.execute("SELECT COUNT(*) as total FROM image_history WHERE " + where_user, params_where)
|
||||||
total = cur.fetchone()['total']
|
total = cur.fetchone()['total']
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def _parse_json(val, default=None):
|
def _parse_json(val, default=None):
|
||||||
if val is None:
|
if val is None:
|
||||||
return default if default is not None else []
|
return default if default is not None else []
|
||||||
if isinstance(val, (list, dict)):
|
if isinstance(val, (list, dict)):
|
||||||
return val
|
return val
|
||||||
try:
|
try:
|
||||||
return json.loads(val)
|
return json.loads(val)
|
||||||
except Exception:
|
except Exception:
|
||||||
return default if default is not None else []
|
return default if default is not None else []
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
items.append({
|
items.append({
|
||||||
'id': r['id'],
|
'id': r['id'],
|
||||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else '',
|
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else '',
|
||||||
'panel_type': r['panel_type'] or '',
|
'panel_type': r['panel_type'] or '',
|
||||||
'original_urls': _parse_json(r['original_urls'], []),
|
'original_urls': _parse_json(r['original_urls'], []),
|
||||||
'params': _parse_json(r['params'], {}),
|
'params': _parse_json(r['params'], {}),
|
||||||
'result_urls': _parse_json(r['result_urls'], []),
|
'result_urls': _parse_json(r['result_urls'], []),
|
||||||
'long_image_url': (r.get('long_image_url') or '').strip() or None,
|
'long_image_url': (r.get('long_image_url') or '').strip() or None,
|
||||||
})
|
})
|
||||||
return jsonify({'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size})
|
return jsonify({'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,69 +1,78 @@
|
|||||||
"""
|
"""
|
||||||
主页面蓝图:首页、home、图片工作台、品牌页、静态文件、Logo
|
主页面蓝图:首页、home、图片工作台、品牌页、静态文件、Logo
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from flask import Blueprint, send_file
|
from flask import Blueprint, send_file
|
||||||
|
|
||||||
from app_common import (
|
from app_common import (
|
||||||
get_db,
|
get_db,
|
||||||
_render_html,
|
_render_html,
|
||||||
_is_session_user_valid,
|
_is_session_user_valid,
|
||||||
login_required,
|
login_required,
|
||||||
admin_required,
|
admin_required,
|
||||||
STATIC_DIR,
|
STATIC_DIR,
|
||||||
BASE_DIR,
|
BASE_DIR,
|
||||||
)
|
)
|
||||||
from flask import redirect, url_for, session
|
from flask import redirect, url_for, session
|
||||||
|
|
||||||
from config import base_url,version
|
from config import base_url,version
|
||||||
|
|
||||||
main_bp = Blueprint('main', __name__)
|
main_bp = Blueprint('main', __name__)
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/')
|
@main_bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
if session.get('user_id') and _is_session_user_valid():
|
if session.get('user_id') and _is_session_user_valid():
|
||||||
return redirect(url_for('main.home'))
|
return redirect(url_for('main.home'))
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/home')
|
@main_bp.route('/home')
|
||||||
@login_required
|
@login_required
|
||||||
def home():
|
def home():
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT username, is_admin FROM users WHERE id = %s", (session['user_id'],))
|
cur.execute("SELECT username, is_admin FROM users WHERE id = %s", (session['user_id'],))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
return _render_html('home.html', username=row.get('username', ''), is_admin=bool(row.get('is_admin')), user_id=session.get('user_id'),baseUrl=base_url,version=version)
|
return _render_html('home.html', username=row.get('username', ''), is_admin=bool(row.get('is_admin')), user_id=session.get('user_id'),baseUrl=base_url,version=version)
|
||||||
except Exception:
|
except Exception:
|
||||||
return _render_html('home.html', username=session.get('username', ''), is_admin=False, user_id=session.get('user_id'),baseUrl=base_url,version=version)
|
return _render_html('home.html', username=session.get('username', ''), is_admin=False, user_id=session.get('user_id'),baseUrl=base_url,version=version)
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/image')
|
@main_bp.route('/image')
|
||||||
@login_required
|
@login_required
|
||||||
def wb():
|
def wb():
|
||||||
return _render_html('index.html')
|
return _render_html('index.html')
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/brand')
|
@main_bp.route('/brand')
|
||||||
@login_required
|
@login_required
|
||||||
def brand_page():
|
def brand_page():
|
||||||
return _render_html('brand.html')
|
return _render_html('brand.html')
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/static/<path:filename>')
|
@main_bp.route('/brand-tools')
|
||||||
def serve_static(filename):
|
@login_required
|
||||||
"""提供 static 目录及子目录下的静态文件访问。"""
|
def brand_tools_page():
|
||||||
filepath = os.path.normpath(os.path.join(STATIC_DIR, filename))
|
html_path = os.path.join(STATIC_DIR, 'brand-tools', 'brand-tools.html')
|
||||||
static_abs = os.path.abspath(STATIC_DIR)
|
if os.path.isfile(html_path):
|
||||||
file_abs = os.path.abspath(filepath)
|
return send_file(html_path)
|
||||||
if not file_abs.startswith(static_abs) or not os.path.isfile(file_abs):
|
return _render_html('brand_tools.html')
|
||||||
return '', 404
|
|
||||||
return send_file(file_abs, as_attachment=False)
|
|
||||||
|
@main_bp.route('/static/<path:filename>')
|
||||||
|
def serve_static(filename):
|
||||||
@main_bp.route('/logo.jpg', methods=['GET'])
|
"""提供 static 目录及子目录下的静态文件访问。"""
|
||||||
def get_logo_image():
|
filepath = os.path.normpath(os.path.join(STATIC_DIR, filename))
|
||||||
return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg')
|
static_abs = os.path.abspath(STATIC_DIR)
|
||||||
|
file_abs = os.path.abspath(filepath)
|
||||||
|
if not file_abs.startswith(static_abs) or not os.path.isfile(file_abs):
|
||||||
|
return '', 404
|
||||||
|
return send_file(file_abs, as_attachment=False)
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route('/logo.jpg', methods=['GET'])
|
||||||
|
def get_logo_image():
|
||||||
|
return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg')
|
||||||
@@ -1,126 +1,126 @@
|
|||||||
"""
|
"""
|
||||||
通过 pywebview 加载本地 HTML,执行 JS 实现解密与 GUID 生成。
|
通过 pywebview 加载本地 HTML,执行 JS 实现解密与 GUID 生成。
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import webview
|
import webview
|
||||||
|
|
||||||
# 内嵌 HTML:加载 CryptoJS 并定义 decryptWithHashSearches、guid
|
# 内嵌 HTML:加载 CryptoJS 并定义 decryptWithHashSearches、guid
|
||||||
_DECRYPT_HTML = """
|
_DECRYPT_HTML = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
function decryptWithHashSearches(ciphertext, hashSearches) {
|
function decryptWithHashSearches(ciphertext, hashSearches) {
|
||||||
try {
|
try {
|
||||||
var baseKey = "8?)i_~Nk6qv0IX;2";
|
var baseKey = "8?)i_~Nk6qv0IX;2";
|
||||||
var keyStr = baseKey + (hashSearches || "");
|
var keyStr = baseKey + (hashSearches || "");
|
||||||
var key = CryptoJS.enc.Utf8.parse(keyStr);
|
var key = CryptoJS.enc.Utf8.parse(keyStr);
|
||||||
|
|
||||||
var decrypted = CryptoJS.AES.decrypt(ciphertext, key, {
|
var decrypted = CryptoJS.AES.decrypt(ciphertext, key, {
|
||||||
mode: CryptoJS.mode.ECB
|
mode: CryptoJS.mode.ECB
|
||||||
});
|
});
|
||||||
|
|
||||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解密失败:', error.message);
|
console.error('解密失败:', error.message);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function guid() {
|
function guid() {
|
||||||
function _p8(s) {
|
function _p8(s) {
|
||||||
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
|
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
|
||||||
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
|
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
|
||||||
}
|
}
|
||||||
return _p8() + _p8(true) + _p8(true) + _p8();
|
return _p8() + _p8(true) + _p8(true) + _p8();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_window = None
|
_window = None
|
||||||
_window_ready = threading.Event()
|
_window_ready = threading.Event()
|
||||||
_init_lock = threading.Lock()
|
_init_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def _ensure_window():
|
def _ensure_window():
|
||||||
"""首次调用时在后台线程启动 pywebview 并等待页面加载完成。"""
|
"""首次调用时在后台线程启动 pywebview 并等待页面加载完成。"""
|
||||||
global _window
|
global _window
|
||||||
with _init_lock:
|
with _init_lock:
|
||||||
if _window is not None:
|
if _window is not None:
|
||||||
return
|
return
|
||||||
_window = webview.create_window(
|
_window = webview.create_window(
|
||||||
"",
|
"",
|
||||||
html=_DECRYPT_HTML,
|
html=_DECRYPT_HTML,
|
||||||
width=1,
|
width=1,
|
||||||
height=1,
|
height=1,
|
||||||
hidden=True,
|
hidden=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
webview.start(debug=False)
|
webview.start(debug=False)
|
||||||
|
|
||||||
t = threading.Thread(target=run, daemon=True)
|
t = threading.Thread(target=run, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
_window.events.loaded.wait()
|
_window.events.loaded.wait()
|
||||||
_window_ready.set()
|
_window_ready.set()
|
||||||
_window_ready.wait()
|
_window_ready.wait()
|
||||||
|
|
||||||
|
|
||||||
def decrypt_via_service(hash_searches, ciphertext):
|
def decrypt_via_service(hash_searches, ciphertext):
|
||||||
"""
|
"""
|
||||||
通过 pywebview 执行 JS decryptWithHashSearches 进行 AES-ECB 解密。
|
通过 pywebview 执行 JS decryptWithHashSearches 进行 AES-ECB 解密。
|
||||||
|
|
||||||
:param hash_searches: 对应 JS 的 hashSearches(密钥后缀,可为空串)
|
:param hash_searches: 对应 JS 的 hashSearches(密钥后缀,可为空串)
|
||||||
:param ciphertext: Base64 密文
|
:param ciphertext: Base64 密文
|
||||||
:return: 解密后的 UTF-8 字符串,失败返回空串
|
:return: 解密后的 UTF-8 字符串,失败返回空串
|
||||||
"""
|
"""
|
||||||
_ensure_window()
|
_ensure_window()
|
||||||
# 将参数安全注入 JS(避免注入与引号问题)
|
# 将参数安全注入 JS(避免注入与引号问题)
|
||||||
ciphertext_js = json.dumps(ciphertext)
|
ciphertext_js = json.dumps(ciphertext)
|
||||||
hash_searches_js = json.dumps(hash_searches or "")
|
hash_searches_js = json.dumps(hash_searches or "")
|
||||||
js = f"(function(){{ return decryptWithHashSearches({ciphertext_js}, {hash_searches_js}); }})();"
|
js = f"(function(){{ return decryptWithHashSearches({ciphertext_js}, {hash_searches_js}); }})();"
|
||||||
try:
|
try:
|
||||||
result = _window.evaluate_js(js)
|
result = _window.evaluate_js(js)
|
||||||
return result if result is not None else ""
|
return result if result is not None else ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"解密失败: {e}") from e
|
raise RuntimeError(f"解密失败: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
def get_guid():
|
def get_guid():
|
||||||
"""通过 pywebview 执行 JS guid() 生成 UUID 格式字符串。"""
|
"""通过 pywebview 执行 JS guid() 生成 UUID 格式字符串。"""
|
||||||
_ensure_window()
|
_ensure_window()
|
||||||
try:
|
try:
|
||||||
result = _window.evaluate_js("(function(){ return guid(); })();")
|
result = _window.evaluate_js("(function(){ return guid(); })();")
|
||||||
if result is None:
|
if result is None:
|
||||||
raise RuntimeError("guid() 返回为空")
|
raise RuntimeError("guid() 返回为空")
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"获取 GUID 失败: {e}") from e
|
raise RuntimeError(f"获取 GUID 失败: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
# 使用示例
|
# 使用示例
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 参数含义:hash_searches 对应原 key,ciphertext 对应原 data
|
# 参数含义:hash_searches 对应原 key,ciphertext 对应原 data
|
||||||
hash_searches = "a6efb809-b714-7efd-4b64-8c650b9030f0"
|
hash_searches = "a6efb809-b714-7efd-4b64-8c650b9030f0"
|
||||||
ciphertext = "SEsfpOGa8B+sWk0ncknTNh/HNHbGiEVi/RNKxBvyHmAE5VjHonPi202c6VicC/GKfA8mLsIC5mGEpSaH2DdCaEJKeOTWD9SBHHbgtyS1O60VqjgAaptYe9LivvWKc/BU8sZOqhxPMzGUHDcKUts7d0p+hCc80XCXyM2ZT80smM1twndcDfpGkLDk2kJbzl2bGzIc60sl9MzKWfZA2sE0ztFjQ9wD2uhn5LrwoN8NnpiPLNbviMMGtHh4N6Dc0xtPzkzgfkiuxBfWnN1SeM9XVgujHvAGea/dqUWIJqLo26fZIOEFJ0MYL4c8CGLYIeP/70cT2IqJdf+IPBWsCiP29zSyAiQd3yLNvd8+xBLky7lR4ng3MZizn/vhW/5BSg1FtVglbAmNbKgHOIbtpP197Lv+uahx2IpsSPqpy4j2O2VV25YzBk9JpJ4WGG/SvF3f2ZYKKepEQ+kGmBxAfG/5Z8DTNIwnOpXhyjFpJb+fUdPV6LTCK/yFc8g31bNFAJkLW7y/VjewjFravZ3VfQjNAHCvifnrIxGG22MZ3TLVnlbh6ye6vTnd+v9GzqXu+ISR7vQGL/ZSM/bJIjuVkP2XnNUPf+NFdt75gyDmTylFmmbpg7WHaBjinPcyVjeZ38+quJhT8yEk66BOjBM47mVdfU8JLK3ToghxQ54dKWGUbc9HRzYIAQ0rIBBExcOcPM4J9DpufvajmEygoDaws4CxOQDlFoFLwNBYorJsKzAoDe1Cu8oFtk4x190Vd+leRKq6DQSNJwEmyrVkWyFpiuywSMaixFlSqve0lRs50FclXfV7gkBAAvz/DJp90i9yCJdMaihP5ZCvbhKxFGMowkU+tx5Ptnxd9hq6tUxHmuiwDI1eyY8EyjB+3MbrC3H/GY39Z5Qhkj5tkSBm4oGA/h6YjeunBlfMU/QGP3hyZIYALh/YmBlZxAcglWwcqlISUhcx1L3Dbw6ZQbpAmYHmA421xIlRJkdJuPmGoopcoFEVdIO0Ov3uP2JL5ybvUgCBMnbwcYRK9qRmvn2b8jHKch55YXerHBDCEMHz88rfgNIEL8DIAp+wMuIyJ0VrV0BwAXioLYNvMcggBp06gghq2NykGiXRZ/dz8nffFqPVkDNjzcPSGrml9fpKdO8x/GannMNBsA+oHlO2/d4dyo+Q2dZCarQ5UB/N1p+UsTW90kvITKwCbZfV/YY2NRm7HrXq+wLS1vY4mChY82Ybc3cYQT653Q=="
|
ciphertext = "SEsfpOGa8B+sWk0ncknTNh/HNHbGiEVi/RNKxBvyHmAE5VjHonPi202c6VicC/GKfA8mLsIC5mGEpSaH2DdCaEJKeOTWD9SBHHbgtyS1O60VqjgAaptYe9LivvWKc/BU8sZOqhxPMzGUHDcKUts7d0p+hCc80XCXyM2ZT80smM1twndcDfpGkLDk2kJbzl2bGzIc60sl9MzKWfZA2sE0ztFjQ9wD2uhn5LrwoN8NnpiPLNbviMMGtHh4N6Dc0xtPzkzgfkiuxBfWnN1SeM9XVgujHvAGea/dqUWIJqLo26fZIOEFJ0MYL4c8CGLYIeP/70cT2IqJdf+IPBWsCiP29zSyAiQd3yLNvd8+xBLky7lR4ng3MZizn/vhW/5BSg1FtVglbAmNbKgHOIbtpP197Lv+uahx2IpsSPqpy4j2O2VV25YzBk9JpJ4WGG/SvF3f2ZYKKepEQ+kGmBxAfG/5Z8DTNIwnOpXhyjFpJb+fUdPV6LTCK/yFc8g31bNFAJkLW7y/VjewjFravZ3VfQjNAHCvifnrIxGG22MZ3TLVnlbh6ye6vTnd+v9GzqXu+ISR7vQGL/ZSM/bJIjuVkP2XnNUPf+NFdt75gyDmTylFmmbpg7WHaBjinPcyVjeZ38+quJhT8yEk66BOjBM47mVdfU8JLK3ToghxQ54dKWGUbc9HRzYIAQ0rIBBExcOcPM4J9DpufvajmEygoDaws4CxOQDlFoFLwNBYorJsKzAoDe1Cu8oFtk4x190Vd+leRKq6DQSNJwEmyrVkWyFpiuywSMaixFlSqve0lRs50FclXfV7gkBAAvz/DJp90i9yCJdMaihP5ZCvbhKxFGMowkU+tx5Ptnxd9hq6tUxHmuiwDI1eyY8EyjB+3MbrC3H/GY39Z5Qhkj5tkSBm4oGA/h6YjeunBlfMU/QGP3hyZIYALh/YmBlZxAcglWwcqlISUhcx1L3Dbw6ZQbpAmYHmA421xIlRJkdJuPmGoopcoFEVdIO0Ov3uP2JL5ybvUgCBMnbwcYRK9qRmvn2b8jHKch55YXerHBDCEMHz88rfgNIEL8DIAp+wMuIyJ0VrV0BwAXioLYNvMcggBp06gghq2NykGiXRZ/dz8nffFqPVkDNjzcPSGrml9fpKdO8x/GannMNBsA+oHlO2/d4dyo+Q2dZCarQ5UB/N1p+UsTW90kvITKwCbZfV/YY2NRm7HrXq+wLS1vY4mChY82Ybc3cYQT653Q=="
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decrypted_text = decrypt_via_service(hash_searches, ciphertext)
|
decrypted_text = decrypt_via_service(hash_searches, ciphertext)
|
||||||
print("解密结果:", decrypted_text)
|
print("解密结果:", decrypted_text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("错误:", e)
|
print("错误:", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
g = get_guid()
|
g = get_guid()
|
||||||
print("GUID:", g)
|
print("GUID:", g)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("GUID 错误:", e)
|
print("GUID 错误:", e)
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
base_url = "https://api.coze.cn/v1"
|
base_url = "https://api.coze.cn/v1"
|
||||||
coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5"
|
coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5"
|
||||||
workflow_id = "7608812635877900322"
|
workflow_id = "7608812635877900322"
|
||||||
STITCH_WORKFLOW_ID = "7608813873483300907"
|
STITCH_WORKFLOW_ID = "7608813873483300907"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
_base_url = os.getenv("base_url","http://159.75.121.33:15124")
|
_base_url = os.getenv("base_url","http://159.75.121.33:15124")
|
||||||
|
|
||||||
# MySQL 配置
|
# MySQL 配置
|
||||||
mysql_host = os.getenv("mysql_host")
|
mysql_host = os.getenv("mysql_host")
|
||||||
mysql_user = os.getenv("mysql_user")
|
mysql_user = os.getenv("mysql_user")
|
||||||
mysql_password = os.getenv("mysql_password","WTFrb5y6hNLz6hNy")
|
mysql_password = os.getenv("mysql_password","WTFrb5y6hNLz6hNy")
|
||||||
mysql_database = os.getenv("mysql_database","aiimage")
|
mysql_database = os.getenv("mysql_database","aiimage")
|
||||||
proxy_url = os.getenv("proxy_url")
|
proxy_url = os.getenv("proxy_url")
|
||||||
proxy_mode = int(os.getenv("proxy_mode",1))
|
proxy_mode = int(os.getenv("proxy_mode",1))
|
||||||
|
|
||||||
client_name=os.getenv("client_name") + ".exe"
|
client_name=os.getenv("client_name") + ".exe"
|
||||||
|
|
||||||
cache_path = "./user_data"
|
cache_path = "./user_data"
|
||||||
|
|
||||||
region = "cn-hangzhou"
|
region = "cn-hangzhou"
|
||||||
endpoint = "oss-cn-hangzhou.aliyuncs.com"
|
endpoint = "oss-cn-hangzhou.aliyuncs.com"
|
||||||
bucket = "nanri-ai-images"
|
bucket = "nanri-ai-images"
|
||||||
accessKeyId = "LTAI5tNpyvzMNz9f2dHarsm8"
|
accessKeyId = "LTAI5tNpyvzMNz9f2dHarsm8"
|
||||||
accessKeySecret = "bQSZnFH455i8tzyOgeahJmUzwmhynz"
|
accessKeySecret = "bQSZnFH455i8tzyOgeahJmUzwmhynz"
|
||||||
bucket_path = "nanri-image/"
|
bucket_path = "nanri-image/"
|
||||||
|
|
||||||
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
||||||
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||||
|
|
||||||
|
|
||||||
debug = True
|
debug = True
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" }
|
APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" }
|
||||||
os.environ['APP_VERSION'] = version
|
os.environ['APP_VERSION'] = version
|
||||||
os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL
|
os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,68 +1,68 @@
|
|||||||
"""
|
"""
|
||||||
HTML 模板加密/解密模块
|
HTML 模板加密/解密模块
|
||||||
使用 AES (Fernet) 对 HTML 资源进行加密存储,运行时解密后渲染
|
使用 AES (Fernet) 对 HTML 资源进行加密存储,运行时解密后渲染
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
|
||||||
|
|
||||||
def _get_fernet_key() -> bytes:
|
def _get_fernet_key() -> bytes:
|
||||||
"""从环境变量 HTML_ENCRYPT_KEY 派生 Fernet 密钥,若未设置则使用默认开发密钥"""
|
"""从环境变量 HTML_ENCRYPT_KEY 派生 Fernet 密钥,若未设置则使用默认开发密钥"""
|
||||||
raw = os.environ.get(
|
raw = os.environ.get(
|
||||||
"HTML_ENCRYPT_KEY",
|
"HTML_ENCRYPT_KEY",
|
||||||
"maixiang_html_encrypt_default_key_change_in_production",
|
"maixiang_html_encrypt_default_key_change_in_production",
|
||||||
)
|
)
|
||||||
# 使用 PBKDF2 派生 32 字节密钥,再转为 Fernet 所需的 base64url
|
# 使用 PBKDF2 派生 32 字节密钥,再转为 Fernet 所需的 base64url
|
||||||
kdf = PBKDF2HMAC(
|
kdf = PBKDF2HMAC(
|
||||||
algorithm=hashes.SHA256(),
|
algorithm=hashes.SHA256(),
|
||||||
length=32,
|
length=32,
|
||||||
salt=b"maixiang_html_salt",
|
salt=b"maixiang_html_salt",
|
||||||
iterations=100000,
|
iterations=100000,
|
||||||
)
|
)
|
||||||
key_bytes = kdf.derive(raw.encode("utf-8"))
|
key_bytes = kdf.derive(raw.encode("utf-8"))
|
||||||
return base64.urlsafe_b64encode(key_bytes)
|
return base64.urlsafe_b64encode(key_bytes)
|
||||||
|
|
||||||
|
|
||||||
def encrypt(plain_data: bytes) -> bytes:
|
def encrypt(plain_data: bytes) -> bytes:
|
||||||
"""加密原始数据,返回密文(bytes)"""
|
"""加密原始数据,返回密文(bytes)"""
|
||||||
key = _get_fernet_key()
|
key = _get_fernet_key()
|
||||||
f = Fernet(key)
|
f = Fernet(key)
|
||||||
return f.encrypt(plain_data)
|
return f.encrypt(plain_data)
|
||||||
|
|
||||||
|
|
||||||
def decrypt(encrypted_data: bytes) -> bytes:
|
def decrypt(encrypted_data: bytes) -> bytes:
|
||||||
"""解密数据,返回明文(bytes)"""
|
"""解密数据,返回明文(bytes)"""
|
||||||
key = _get_fernet_key()
|
key = _get_fernet_key()
|
||||||
f = Fernet(key)
|
f = Fernet(key)
|
||||||
return f.decrypt(encrypted_data)
|
return f.decrypt(encrypted_data)
|
||||||
|
|
||||||
|
|
||||||
def encrypt_file(path: str) -> None:
|
def encrypt_file(path: str) -> None:
|
||||||
"""就地加密文件:读取 UTF-8 内容,加密后写回同一路径"""
|
"""就地加密文件:读取 UTF-8 内容,加密后写回同一路径"""
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
plain = f.read()
|
plain = f.read()
|
||||||
encrypted = encrypt(plain)
|
encrypted = encrypt(plain)
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(encrypted)
|
f.write(encrypted)
|
||||||
|
|
||||||
|
|
||||||
def decrypt_file(path: str) -> None:
|
def decrypt_file(path: str) -> None:
|
||||||
"""就地解密文件:读取密文,解密后以 UTF-8 写回同一路径"""
|
"""就地解密文件:读取密文,解密后以 UTF-8 写回同一路径"""
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
encrypted = f.read()
|
encrypted = f.read()
|
||||||
plain = decrypt(encrypted)
|
plain = decrypt(encrypted)
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(plain)
|
f.write(plain)
|
||||||
|
|
||||||
|
|
||||||
def is_encrypted(data: bytes) -> bool:
|
def is_encrypted(data: bytes) -> bool:
|
||||||
"""简单启发式:Fernet 密文以 b'gAAAAA' 开头(base64 编码后)"""
|
"""简单启发式:Fernet 密文以 b'gAAAAA' 开头(base64 编码后)"""
|
||||||
try:
|
try:
|
||||||
return data[:7] == b"gAAAAAB"
|
return data[:7] == b"gAAAAAB"
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
@@ -1,249 +1,249 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class DeviceIDGenerator:
|
class DeviceIDGenerator:
|
||||||
"""
|
"""
|
||||||
Windows设备唯一ID生成器
|
Windows设备唯一ID生成器
|
||||||
通过收集多个硬件特征来生成稳定的设备唯一标识符
|
通过收集多个硬件特征来生成稳定的设备唯一标识符
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, use_cache: bool = False, cache_file: str = ".device_id"):
|
def __init__(self, use_cache: bool = False, cache_file: str = ".device_id"):
|
||||||
"""
|
"""
|
||||||
初始化设备ID生成器
|
初始化设备ID生成器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
use_cache: 是否使用本地缓存
|
use_cache: 是否使用本地缓存
|
||||||
cache_file: 缓存文件名
|
cache_file: 缓存文件名
|
||||||
"""
|
"""
|
||||||
self.use_cache = use_cache
|
self.use_cache = use_cache
|
||||||
self.cache_file = cache_file
|
self.cache_file = cache_file
|
||||||
|
|
||||||
def _run_wmic_command(self, command: str) -> Optional[str]:
|
def _run_wmic_command(self, command: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
执行WMIC命令并返回结果
|
执行WMIC命令并返回结果
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
command: WMIC命令
|
command: WMIC命令
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
命令执行结果,失败则返回None
|
命令执行结果,失败则返回None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
command,
|
command,
|
||||||
shell=True,
|
shell=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
if result.returncode == 0 and result.stdout.strip():
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
except (subprocess.TimeoutExpired, Exception):
|
except (subprocess.TimeoutExpired, Exception):
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_motherboard_serial(self) -> Optional[str]:
|
def _get_motherboard_serial(self) -> Optional[str]:
|
||||||
"""获取主板序列号"""
|
"""获取主板序列号"""
|
||||||
return self._run_wmic_command("wmic baseboard get serialnumber /value")
|
return self._run_wmic_command("wmic baseboard get serialnumber /value")
|
||||||
|
|
||||||
def _get_cpu_id(self) -> Optional[str]:
|
def _get_cpu_id(self) -> Optional[str]:
|
||||||
"""获取CPU ID"""
|
"""获取CPU ID"""
|
||||||
return self._run_wmic_command("wmic cpu get processorid /value")
|
return self._run_wmic_command("wmic cpu get processorid /value")
|
||||||
|
|
||||||
def _get_bios_serial(self) -> Optional[str]:
|
def _get_bios_serial(self) -> Optional[str]:
|
||||||
"""获取BIOS序列号"""
|
"""获取BIOS序列号"""
|
||||||
return self._run_wmic_command("wmic bios get serialnumber /value")
|
return self._run_wmic_command("wmic bios get serialnumber /value")
|
||||||
|
|
||||||
def _get_disk_serial(self) -> Optional[str]:
|
def _get_disk_serial(self) -> Optional[str]:
|
||||||
"""获取系统盘序列号"""
|
"""获取系统盘序列号"""
|
||||||
return self._run_wmic_command("wmic diskdrive get serialnumber /value")
|
return self._run_wmic_command("wmic diskdrive get serialnumber /value")
|
||||||
|
|
||||||
def _get_machine_guid(self) -> Optional[str]:
|
def _get_machine_guid(self) -> Optional[str]:
|
||||||
"""获取Windows机器GUID"""
|
"""获取Windows机器GUID"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
|
'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
|
||||||
shell=True,
|
shell=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
for line in result.stdout.split('\n'):
|
for line in result.stdout.split('\n'):
|
||||||
if 'MachineGuid' in line:
|
if 'MachineGuid' in line:
|
||||||
return line.split()[-1]
|
return line.split()[-1]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _extract_value(self, wmic_output: str) -> str:
|
def _extract_value(self, wmic_output: str) -> str:
|
||||||
"""从WMIC输出中提取实际值"""
|
"""从WMIC输出中提取实际值"""
|
||||||
if not wmic_output:
|
if not wmic_output:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
lines = wmic_output.split('\n')
|
lines = wmic_output.split('\n')
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if '=' in line and not line.strip().endswith('='):
|
if '=' in line and not line.strip().endswith('='):
|
||||||
return line.split('=', 1)[1].strip()
|
return line.split('=', 1)[1].strip()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _collect_hardware_info(self) -> dict:
|
def _collect_hardware_info(self) -> dict:
|
||||||
"""
|
"""
|
||||||
收集硬件信息
|
收集硬件信息
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
包含各种硬件信息的字典
|
包含各种硬件信息的字典
|
||||||
"""
|
"""
|
||||||
hardware_info = {}
|
hardware_info = {}
|
||||||
|
|
||||||
# 主板序列号
|
# 主板序列号
|
||||||
motherboard = self._get_motherboard_serial()
|
motherboard = self._get_motherboard_serial()
|
||||||
hardware_info['motherboard'] = self._extract_value(motherboard) if motherboard else ""
|
hardware_info['motherboard'] = self._extract_value(motherboard) if motherboard else ""
|
||||||
|
|
||||||
# CPU ID
|
# CPU ID
|
||||||
cpu_id = self._get_cpu_id()
|
cpu_id = self._get_cpu_id()
|
||||||
hardware_info['cpu'] = self._extract_value(cpu_id) if cpu_id else ""
|
hardware_info['cpu'] = self._extract_value(cpu_id) if cpu_id else ""
|
||||||
|
|
||||||
# BIOS序列号
|
# BIOS序列号
|
||||||
bios = self._get_bios_serial()
|
bios = self._get_bios_serial()
|
||||||
hardware_info['bios'] = self._extract_value(bios) if bios else ""
|
hardware_info['bios'] = self._extract_value(bios) if bios else ""
|
||||||
|
|
||||||
# 硬盘序列号
|
# 硬盘序列号
|
||||||
disk = self._get_disk_serial()
|
disk = self._get_disk_serial()
|
||||||
hardware_info['disk'] = self._extract_value(disk) if disk else ""
|
hardware_info['disk'] = self._extract_value(disk) if disk else ""
|
||||||
|
|
||||||
# Windows机器GUID
|
# Windows机器GUID
|
||||||
machine_guid = self._get_machine_guid()
|
machine_guid = self._get_machine_guid()
|
||||||
hardware_info['machine_guid'] = machine_guid if machine_guid else ""
|
hardware_info['machine_guid'] = machine_guid if machine_guid else ""
|
||||||
|
|
||||||
# 计算机名称
|
# 计算机名称
|
||||||
hardware_info['computer_name'] = platform.node()
|
hardware_info['computer_name'] = platform.node()
|
||||||
|
|
||||||
# MAC地址(作为备用)
|
# MAC地址(作为备用)
|
||||||
hardware_info['mac_address'] = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
|
hardware_info['mac_address'] = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
|
||||||
for elements in range(0, 2*6, 2)][::-1])
|
for elements in range(0, 2*6, 2)][::-1])
|
||||||
|
|
||||||
return hardware_info
|
return hardware_info
|
||||||
|
|
||||||
def _generate_device_id(self, hardware_info: dict) -> str:
|
def _generate_device_id(self, hardware_info: dict) -> str:
|
||||||
"""
|
"""
|
||||||
基于硬件信息生成设备ID
|
基于硬件信息生成设备ID
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
hardware_info: 硬件信息字典
|
hardware_info: 硬件信息字典
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
32位十六进制设备ID
|
32位十六进制设备ID
|
||||||
"""
|
"""
|
||||||
# 过滤掉空值,并按键排序确保一致性
|
# 过滤掉空值,并按键排序确保一致性
|
||||||
filtered_info = {k: v for k, v in hardware_info.items() if v and v.strip()}
|
filtered_info = {k: v for k, v in hardware_info.items() if v and v.strip()}
|
||||||
|
|
||||||
# 如果没有任何硬件信息,使用MAC地址作为后备方案
|
# 如果没有任何硬件信息,使用MAC地址作为后备方案
|
||||||
if not filtered_info:
|
if not filtered_info:
|
||||||
filtered_info = {'mac_address': hardware_info.get('mac_address', str(uuid.getnode()))}
|
filtered_info = {'mac_address': hardware_info.get('mac_address', str(uuid.getnode()))}
|
||||||
|
|
||||||
# 将所有信息连接成字符串
|
# 将所有信息连接成字符串
|
||||||
info_string = '|'.join(f"{k}:{v}" for k, v in sorted(filtered_info.items()))
|
info_string = '|'.join(f"{k}:{v}" for k, v in sorted(filtered_info.items()))
|
||||||
|
|
||||||
# 使用SHA256生成哈希值
|
# 使用SHA256生成哈希值
|
||||||
hash_object = hashlib.sha256(info_string.encode('utf-8'))
|
hash_object = hashlib.sha256(info_string.encode('utf-8'))
|
||||||
device_id = hash_object.hexdigest()
|
device_id = hash_object.hexdigest()
|
||||||
|
|
||||||
return device_id
|
return device_id
|
||||||
|
|
||||||
def _load_cached_device_id(self) -> Optional[str]:
|
def _load_cached_device_id(self) -> Optional[str]:
|
||||||
"""从缓存文件加载设备ID"""
|
"""从缓存文件加载设备ID"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(self.cache_file):
|
if os.path.exists(self.cache_file):
|
||||||
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
||||||
cached_id = f.read().strip()
|
cached_id = f.read().strip()
|
||||||
if len(cached_id) == 64: # SHA256哈希长度
|
if len(cached_id) == 64: # SHA256哈希长度
|
||||||
return cached_id
|
return cached_id
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _save_device_id_to_cache(self, device_id: str) -> None:
|
def _save_device_id_to_cache(self, device_id: str) -> None:
|
||||||
"""将设备ID保存到缓存文件"""
|
"""将设备ID保存到缓存文件"""
|
||||||
try:
|
try:
|
||||||
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
||||||
f.write(device_id)
|
f.write(device_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_device_id(self) -> str:
|
def get_device_id(self) -> str:
|
||||||
"""
|
"""
|
||||||
获取设备唯一ID
|
获取设备唯一ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
64字符的十六进制设备ID
|
64字符的十六进制设备ID
|
||||||
"""
|
"""
|
||||||
# 如果启用缓存,先尝试从缓存加载
|
# 如果启用缓存,先尝试从缓存加载
|
||||||
if self.use_cache:
|
if self.use_cache:
|
||||||
cached_id = self._load_cached_device_id()
|
cached_id = self._load_cached_device_id()
|
||||||
if cached_id:
|
if cached_id:
|
||||||
return cached_id
|
return cached_id
|
||||||
|
|
||||||
# 收集硬件信息
|
# 收集硬件信息
|
||||||
hardware_info = self._collect_hardware_info()
|
hardware_info = self._collect_hardware_info()
|
||||||
|
|
||||||
# 生成设备ID
|
# 生成设备ID
|
||||||
device_id = self._generate_device_id(hardware_info)
|
device_id = self._generate_device_id(hardware_info)
|
||||||
|
|
||||||
# 保存到缓存
|
# 保存到缓存
|
||||||
if self.use_cache:
|
if self.use_cache:
|
||||||
self._save_device_id_to_cache(device_id)
|
self._save_device_id_to_cache(device_id)
|
||||||
|
|
||||||
return device_id
|
return device_id
|
||||||
|
|
||||||
def get_device_id_short(self, length: int = 16) -> str:
|
def get_device_id_short(self, length: int = 16) -> str:
|
||||||
"""
|
"""
|
||||||
获取短版本的设备ID
|
获取短版本的设备ID
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
length: 返回ID的长度
|
length: 返回ID的长度
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
指定长度的设备ID
|
指定长度的设备ID
|
||||||
"""
|
"""
|
||||||
full_id = self.get_device_id()
|
full_id = self.get_device_id()
|
||||||
return full_id[:length]
|
return full_id[:length]
|
||||||
|
|
||||||
def get_hardware_info(self) -> dict:
|
def get_hardware_info(self) -> dict:
|
||||||
"""
|
"""
|
||||||
获取硬件信息(用于调试)
|
获取硬件信息(用于调试)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
硬件信息字典
|
硬件信息字典
|
||||||
"""
|
"""
|
||||||
return self._collect_hardware_info()
|
return self._collect_hardware_info()
|
||||||
|
|
||||||
|
|
||||||
# 使用示例
|
# 使用示例
|
||||||
def main():
|
def main():
|
||||||
"""使用示例"""
|
"""使用示例"""
|
||||||
# 创建设备ID生成器实例
|
# 创建设备ID生成器实例
|
||||||
device_generator = DeviceIDGenerator()
|
device_generator = DeviceIDGenerator()
|
||||||
|
|
||||||
# 获取完整设备ID(64字符)
|
# 获取完整设备ID(64字符)
|
||||||
device_id = device_generator.get_device_id()
|
device_id = device_generator.get_device_id()
|
||||||
print(f"完整设备ID: {device_id}")
|
print(f"完整设备ID: {device_id}")
|
||||||
|
|
||||||
# 获取短版本设备ID(16字符)
|
# 获取短版本设备ID(16字符)
|
||||||
short_id = device_generator.get_device_id_short(16)
|
short_id = device_generator.get_device_id_short(16)
|
||||||
print(f"短设备ID: {short_id}")
|
print(f"短设备ID: {short_id}")
|
||||||
|
|
||||||
# 查看硬件信息(调试用)
|
# 查看硬件信息(调试用)
|
||||||
hardware_info = device_generator.get_hardware_info()
|
hardware_info = device_generator.get_hardware_info()
|
||||||
print("\n硬件信息:")
|
print("\n硬件信息:")
|
||||||
for key, value in hardware_info.items():
|
for key, value in hardware_info.items():
|
||||||
print(f" {key}: {value}")
|
print(f" {key}: {value}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
14
source_code/web_source/brand_tools.html
Normal file
14
source_code/web_source/brand_tools.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="refresh" content="0; url=/static/brand-tools/brand-tools.html" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>品牌工具 - 数富AI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.location.replace('/static/brand-tools/brand-tools.html')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user