打包项目

This commit is contained in:
super
2026-03-20 18:13:47 +08:00
parent 7c6d9dd989
commit 63d3e4047c
59 changed files with 1872 additions and 4187 deletions

12
frontend-vue/convert.html Normal file
View 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
View 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
View 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>

View File

@@ -1,3 +0,0 @@
<template>
<router-view />
</template>

View 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')

View 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')

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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`)
}

View File

@@ -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
}
}

View File

@@ -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,
})
}

View File

@@ -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')
}

View File

@@ -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 })
}

View File

@@ -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
} }

View File

@@ -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,
}
}

View File

@@ -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')

View File

@@ -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]',
},
},
}, },
}) })

View File

@@ -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")

View File

@@ -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

View File

@@ -1 +1 @@
# 蓝图包 # 蓝图包

View File

@@ -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)})

View File

@@ -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'))

View File

@@ -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)})

View File

@@ -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')

View File

@@ -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 对应原 keyciphertext 对应原 data # 参数含义hash_searches 对应原 keyciphertext 对应原 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)

View File

@@ -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

View File

@@ -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

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -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()
# 获取完整设备ID64字符 # 获取完整设备ID64字符
device_id = device_generator.get_device_id() device_id = device_generator.get_device_id()
print(f"完整设备ID: {device_id}") print(f"完整设备ID: {device_id}")
# 获取短版本设备ID16字符 # 获取短版本设备ID16字符
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()

View 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>