Files
leonapp/developer/upload_app.php
Leonmmcoset 971e3b8d58 refactor(assets): 将CDN资源替换为本地文件并添加推荐功能
将Bootstrap和Font Awesome从CDN引用改为本地文件引用,提高加载可靠性
在首页为登录用户添加基于标签的个性化应用推荐功能
使用SweetAlert2替换原生alert提升用户体验
2025-07-07 16:40:02 +08:00

481 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// 引入配置文件
require_once '../config.php';
session_start();
// 检查开发者是否已登录
if (!isset($_SESSION['developer_id'])) {
header('Location: login.php');
exit;
}
$developerId = $_SESSION['developer_id'];
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 创建上传目录(如果不存在)
$uploadDirs = ['../uploads/apps', '../uploads/images'];
foreach ($uploadDirs as $dir) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
// 获取表单数据
$appName = trim($_POST['name']);
$appDescription = trim($_POST['description']);
$tags = $_POST['tags'] ?? [];
$ageRating = $_POST['age_rating'] ?? '';
$ageRatingDescription = $_POST['age_rating_description'] ?? '';
$platforms = isset($_POST['platforms']) ? $_POST['platforms'] : [];
$version = trim($_POST['version']);
$changelog = trim($_POST['changelog']);
// 验证表单数据
if (empty($appName) || empty($appDescription)) {
$error = '应用名称和描述不能为空';
} elseif (empty($version) || !preg_match('/^\d+\.\d+\.\d+$/', $version)) {
$error = '版本号格式不正确应为X.X.X格式';
} elseif (empty($changelog)) {
$error = '更新日志不能为空';
} elseif (empty($platforms)) {
$error = '请至少选择一个适用平台';
} elseif (in_array($ageRating, ['12+', '17+']) && empty($ageRatingDescription)) {
$error = '年龄分级为12+或以上时,必须提供年龄分级说明';
} else {
// 检查数据库连接是否为 MySQLi 对象
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
// 处理应用文件上传
// 获取选中的平台
$selectedPlatforms = $_POST['platforms'] ?? [];
// 处理应用文件上传
$appFile = $_FILES['app_file'] ?? null;
$appFilePath = '';
if ($appFile && $appFile['error'] === UPLOAD_ERR_OK) {
// 验证文件大小 (100MB)
if ($appFile['size'] > 100 * 1024 * 1024) {
log_error('应用文件过大: ' . number_format($appFile['size'] / 1024 / 1024, 2) . 'MB', __FILE__, __LINE__);
$error = '应用文件大小不能超过100MB';
}
$appExtension = pathinfo($appFile['name'], PATHINFO_EXTENSION);
$appFileName = uniqid() . '.' . $appExtension;
$appRelativePath = 'uploads/apps/' . $appFileName;
$appFilePath = __DIR__ . '/../' . $appRelativePath;
if (!move_uploaded_file($appFile['tmp_name'], $appFilePath)) {
log_error('应用文件移动失败', __FILE__, __LINE__);
$error = '应用文件上传失败';
}
} else {
$error = '应用文件上传错误: ' . ($appFile ? $appFile['error'] : '未找到文件');
}
// 处理图片上传
$imagePaths = [];
$images = $_FILES['images'] ?? null;
if ($images && is_array($images['tmp_name'])) {
foreach ($images['tmp_name'] as $key => $tmpName) {
if ($images['error'][$key] === UPLOAD_ERR_OK) {
// 验证图片大小 (10MB)
if ($images['size'][$key] > 10 * 1024 * 1024) {
log_error('图片过大: ' . $images['name'][$key] . ' (' . number_format($images['size'][$key] / 1024 / 1024, 2) . 'MB)', __FILE__, __LINE__);
$error = '图片 ' . $images['name'][$key] . ' 大小不能超过10MB';
}
$imageRelativePath = 'uploads/images/' . uniqid() . '.' . pathinfo($images['name'][$key], PATHINFO_EXTENSION);
$imagePath = __DIR__ . '/../' . $imageRelativePath;
$target_dir = dirname($imagePath);
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true);
}
if (move_uploaded_file($tmpName, $imagePath)) {
$imagePaths[] = $imagePath;
} else {
log_error('图片文件移动失败: ' . $images['name'][$key], __FILE__, __LINE__);
}
}
}
}
if (empty($error)) {
// 开始数据库事务
$conn->begin_transaction();
try {
// 确保必要变量存在,防止空值导致 SQL 错误
if (!isset($appName) || !isset($appDescription) || !isset($developerId) || !isset($version) || !isset($changelog) || !isset($ageRating) || !isset($ageRatingDescription)) {
throw new Exception('缺少必要的上传参数');
}
// 插入应用基本信息
$filePath = ''; // 初始化file_path为空字符串以满足数据库要求
// 添加created_at字段并设置为当前时间戳
$stmt = $conn->prepare('INSERT INTO apps (name, description, developer_id, platforms, status, age_rating, age_rating_description, version, changelog, file_path, created_at) VALUES (?, ?, ?, ?, \'pending\', ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)');
if (!$stmt) {
throw new Exception('应用基本信息查询准备失败: ' . $conn->error);
}
// 确保平台数据正确编码
$platforms = $_POST['platforms'] ?? [];
$platforms_json = json_encode($platforms);
// 此处需确认预处理语句占位符数量,确保与 bind_param 参数数量一致,示例仅示意,实际需根据表结构调整
// 修正参数绑定添加file_path参数以匹配SQL占位符数量
// 修正参数类型字符串长度确保与10个参数匹配
// 修正类型字符串长度10个参数对应10个类型字符
// 最终修正10个参数对应10个类型字符
// 根据参数实际类型修正类型字符串整数用i字符串用s
// 移除多余的$status参数匹配SQL中9个占位符
// 修正age_rating_description类型为字符串并确保9个参数与占位符匹配
// 修复变量名错误:使用已验证的$appFilePath替换未定义的$file_path
$stmt->bind_param('ssissssss', $appName, $appDescription, $developerId, $platforms_json, $ageRating, $ageRatingDescription, $version, $changelog, $filePath);
if (!$stmt->execute()) {
throw new Exception('应用基本信息查询执行失败: ' . $stmt->error);
}
$appId = $stmt->insert_id;
$stmt->close();
// 插入应用标签关联
foreach ($tags as $tagId) {
$tagStmt = $conn->prepare('INSERT INTO app_tags (app_id, tag_id) VALUES (?, ?)');
if (!$tagStmt) {
throw new Exception('标签关联查询准备失败: ' . $conn->error);
}
$tagStmt->bind_param('ii', $appId, $tagId);
if (!$tagStmt->execute()) {
throw new Exception('标签关联查询执行失败: ' . $tagStmt->error);
}
$tagStmt->close();
}
// 插入应用图片
foreach ($imagePaths as $imagePath) {
$imageStmt = $conn->prepare('INSERT INTO app_images (app_id, image_path) VALUES (?, ?)');
if (!$imageStmt) {
throw new Exception('图片关联查询准备失败: ' . $conn->error);
}
$imageStmt->bind_param('is', $appId, $imageRelativePath);
if (!$imageStmt->execute()) {
throw new Exception('图片关联查询执行失败: ' . $imageStmt->error);
}
$imageStmt->close();
}
// 插入应用版本信息
$versionStmt = $conn->prepare('INSERT INTO app_versions (app_id, version, changelog, file_path, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)');
if (!$versionStmt) {
throw new Exception('版本信息查询准备失败: ' . $conn->error);
}
$versionStmt->bind_param('isss', $appId, $version, $changelog, $appRelativePath);
if (!$versionStmt->execute()) {
throw new Exception('版本信息查询执行失败: ' . $versionStmt->error);
}
$versionStmt->close();
// 提交事务
$conn->commit();
$success = '应用上传成功,请等待管理员审核';
} catch (Exception $e) {
// 回滚事务
$conn->rollback();
log_error('应用上传事务失败: ' . $e->getMessage(), __FILE__, __LINE__);
$error = '上传应用时发生错误,请稍后再试';
}
}
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传应用 - <?php echo APP_STORE_NAME; ?></title>
<!-- Bootstrap CSS -->
<link href="../css/bootstrap.min.css" rel="stylesheet">
<!-- 自定义CSS -->
<link rel="stylesheet" href="../styles.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Fluent Design 模糊效果 -->
<style>
.blur-bg {
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.5);
}
.form-group {
margin-bottom: 1rem;
}
.btn-primary {
background-color: #007BFF;
border-color: #007BFF;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.back-link {
text-align: center;
margin-top: 1rem;
}
#ageRatingDescriptionGroup {
display: none;
}
</style>
<!-- Bootstrap JS with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</head>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light blur-bg">
<div class="container">
<a class="navbar-brand" href="../index.php"><?php echo APP_STORE_NAME; ?></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="dashboard.php">应用仪表盘</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="upload_app.php">上传应用</a>
</li>
<li class="nav-item">
<a class="nav-link" href="logout.php">退出登录</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<style>
.form-group {
margin-bottom: 1rem;
}
.btn-primary {
background-color: #007BFF;
border-color: #007BFF;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.back-link {
text-align: center;
margin-top: 1rem;
}
#ageRatingDescriptionGroup {
display: none;
}
</style>
<script>
// 年龄分级说明显示控制
document.addEventListener('DOMContentLoaded', function() {
const ageRating = document.getElementById('age_rating');
const ageDescGroup = document.getElementById('ageRatingDescriptionGroup');
const ageDescInput = document.getElementById('age_rating_description');
function toggleAgeDescription() {
if (['12+', '17+'].includes(ageRating.value)) {
ageDescGroup.style.display = 'block';
ageDescInput.required = true;
} else {
ageDescGroup.style.display = 'none';
ageDescInput.required = false;
}
}
ageRating.addEventListener('change', toggleAgeDescription);
toggleAgeDescription(); // 初始状态检查
// 文件类型验证
const appFileInput = document.getElementById('app_file');
const imageInput = document.getElementById('images');
const allowedAppTypes = { 'android': ['apk'], 'ios': ['ipa'] };
const allowedImageTypes = ['jpg', 'jpeg', 'png', 'gif'];
appFileInput.addEventListener('change', function(e) {
if (this.files.length > 0) {
const file = this.files[0];
const ext = file.name.split('.').pop().toLowerCase();
if (file.size > 100 * 1024 * 1024) { // 100MB限制
Swal.fire({
title: '提示',
text: '文件大小不能超过100MB',
icon: 'warning',
confirmButtonText: '确定'
});
this.value = '';
}
}
});
imageInput.addEventListener('change', function(e) {
if (this.files.length > 0) {
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
if (file.size > 10 * 1024 * 1024) { // 10MB限制
Swal.fire({
title: '提示',
text: `图片 ${file.name} 大小不能超过10MB`,
icon: 'warning',
confirmButtonText: '确定'
});
this.value = '';
return;
}
}
}
});
// 平台子选项显示控制
document.getElementById('windows').addEventListener('change', function() {
const suboptions = document.getElementById('windows_suboptions');
suboptions.style.display = this.checked ? 'block' : 'none';
if (!this.checked) {
document.querySelectorAll('input[name="windows_version"]').forEach(radio => radio.checked = false);
}
});
document.getElementById('linux').addEventListener('change', function() {
const suboptions = document.getElementById('linux_suboptions');
suboptions.style.display = this.checked ? 'block' : 'none';
if (!this.checked) {
document.querySelectorAll('input[name="linux_distribution"]').forEach(radio => radio.checked = false);
}
});
});
</script>
</head>
<body>
<div class="container mt-5 mb-5 col-md-8 col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">上传应用</h2>
</div>
<div class="card-body p-4">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $error; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php echo $success; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data">
<div class="form-group mb-3">
<label for="name" class="form-label">应用名称</label>
<input type="text" id="name" name="name" class="form-control" required>
</div>
<div class="form-group mb-3">
<label for="tags" class="form-label">标签</label>
<select id="tags" name="tags[]" multiple class="form-select" size="3">
<?php
$tagResult = $conn->query("SELECT id, name FROM tags");
while ($tag = $tagResult->fetch_assoc()):
?>
<option value="<?php echo $tag['id']; ?>"><?php echo htmlspecialchars($tag['name']); ?></option>
<?php endwhile; ?>
</select>
<small>按住Ctrl键可选择多个标签</small>
</div>
<div class="form-group mb-3">
<label for="description" class="form-label">应用描述</label>
<textarea id="description" name="description" rows="5" class="form-control" required></textarea>
</div>
<div class="form-group mb-3">
<label for="age_rating" class="form-label">年龄分级</label>
<select class="form-select" id="age_rating" name="age_rating" required>
<option value="3+">3+</option>
<option value="7+">7+</option>
<option value="12+">12+</option>
<option value="17+">17+</option>
</select>
</div>
<div class="form-group mb-3" id="ageRatingDescriptionGroup">
<label for="age_rating_description" class="form-label">年龄分级说明</label>
<textarea class="form-control" id="age_rating_description" name="age_rating_description" rows="3" placeholder="请说明为何需要此年龄分级"></textarea>
<small>当年龄分级为12+或以上时,此项为必填</small>
</div>
<div class="form-group mb-3">
<label class="form-label">适用平台</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="android" id="android" name="platforms[]">
<label class="form-check-label" for="android">Android</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="ios" id="ios" name="platforms[]">
<label class="form-check-label" for="ios">iOS</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="windows" id="windows" name="platforms[]">
<label class="form-check-label" for="windows">Windows</label>
</div>
<div id="windows_suboptions" class="ms-4 mt-2" style="display: none;">
<div class="form-check">
<input class="form-check-input" type="radio" name="windows_version" id="windows_xp" value="windows_xp">
<label class="form-check-label" for="windows_xp">XP以前</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="windows_version" id="windows_win7" value="windows_win7">
<label class="form-check-label" for="windows_win7">Win7以后</label>
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="macos" id="macos" name="platforms[]">
<label class="form-check-label" for="macos">macOS</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="linux" id="linux" name="platforms[]">
<label class="form-check-label" for="linux">Linux</label>
</div>
<div id="linux_suboptions" class="ms-4 mt-2" style="display: none;">
<div class="form-check">
<input class="form-check-input" type="radio" name="linux_distribution" id="linux_ubuntu" value="linux_ubuntu">
<label class="form-check-label" for="linux_ubuntu">Ubuntu</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="linux_distribution" id="linux_arch" value="linux_arch">
<label class="form-check-label" for="linux_arch">Arch Linux</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="linux_distribution" id="linux_centos" value="linux_centos">
<label class="form-check-label" for="linux_centos">CentOS</label>
</div>
</div>
</div>
<div class="form-group mb-3">
<label for="app_file" class="form-label">应用文件</label>
<input type="file" id="app_file" name="app_file" class="form-control" required>
</div>
<div class="form-group mb-3">
<label for="version" class="form-label">版本号</label>
<input type="text" id="version" name="version" class="form-control" required placeholder="例如: 1.0.0">
</div>
<div class="form-group mb-3">
<label for="changelog" class="form-label">更新日志</label>
<textarea id="changelog" name="changelog" rows="3" class="form-control" required></textarea>
</div>
<div class="form-group mb-4">
<label for="images" class="form-label">预览图片</label>
<input type="file" id="images" name="images[]" multiple accept="image/*" class="form-control">
<small>可选择多张图片</small>
</div>
<input type="submit" value="上传" class="btn btn-primary w-100 py-2">
</form>
<div class="back-link mt-4">
<a href="dashboard.php" class="btn btn-outline-secondary w-100">返回仪表盘</a>
</div>
</div>
</div>
</div>
</body>
</html>