feat: 添加开发者功能模块和审核系统

- 新增开发者注册、登录、仪表盘和登出功能
- 实现应用上传、编辑和管理功能
- 添加管理员审核应用和管理开发者功能
- 完善数据库结构支持开发者系统
- 增加错误日志记录功能
- 更新.gitignore忽略上传目录和系统文件
This commit is contained in:
2025-07-07 14:52:53 +08:00
parent d7aea6a1c4
commit 1e520e9f26
13 changed files with 1951 additions and 4 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# 忽略上传的应用文件
uploads/
# 忽略文件目录
files/
# 忽略图片目录
images/
# 忽略日志文件
logs/
# 忽略Windows系统文件
Thumbs.db
.DS_Store

View File

@@ -16,7 +16,7 @@ if (isset($_GET['logout'])) {
}
// 获取App列表
$sqlApps = "SELECT * FROM apps ORDER BY created_at DESC";
$sqlApps = "SELECT * FROM apps WHERE status = 'approved' ORDER BY created_at DESC";
$resultApps = $conn->query($sqlApps);
if (!$resultApps) {
@@ -56,7 +56,13 @@ if (!$resultApps) {
<a class="nav-link active" aria-current="page" href="index.php">App列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="addapp.php">添加App</a>
<a class="nav-link" href="addapp.php">添加App</a>
</li>
<li class="nav-item">
<a class="nav-link" href="review_apps.php">审核APP</a>
</li>
<li class="nav-item">
<a class="nav-link" href="manage_developers.php">管理开发者</a>
</li>
<li class="nav-item">
<a class="nav-link" href="?logout=true">退出登录</a>

246
admin/manage_developers.php Normal file
View File

@@ -0,0 +1,246 @@
<?php
require_once '../config.php';
// 检查管理员权限
// 设置会话cookie路径为根目录以确保跨目录访问
session_set_cookie_params(0, '/');
// 检查会话是否已启动,避免重复启动
if (session_status() == PHP_SESSION_NONE) {
if (!session_start()) {
error_log('会话启动失败');
header('Location: login.php');
exit;
error_log('会话启动失败');
header('Location: login.php');
exit;
// 从数据库验证用户角色,确保权限检查准确性
if (isset($_SESSION['user_id'])) {
$userId = $_SESSION['user_id'];
$stmt = $conn->prepare("SELECT role FROM users WHERE id = ?");
if (!$stmt) {
error_log('Database prepare failed: ' . $conn->error);
header('Location: login.php');
exit;
}
$stmt->bind_param("i", $userId);
if (!$stmt->execute()) {
error_log('Query execution failed: ' . $stmt->error);
header('Location: login.php');
exit;
}
$result = $stmt->get_result();
if (!$result) {
error_log('Failed to get result: ' . $stmt->error);
header('Location: login.php');
exit;
}
$user = $result->fetch_assoc();
if (!$user || $user['role'] !== 'admin') {
error_log('用户 ' . $userId . ' 不是管理员,拒绝访问');
header('Location: login.php');
exit;
}
} else {
error_log('未找到用户会话,重定向到登录页');
header('Location: login.php');
exit;
}
}
}
// 处理删除用户请求
if (isset($_POST['delete_user'])) {
$userId = $_POST['user_id'];
$stmt = $conn->prepare("DELETE FROM users WHERE id = ? AND role = 'developer'");
$stmt->bind_param("i", $userId);
$stmt->execute();
$stmt->close();
header("Location: manage_developers.php?deleted=true");
exit;
}
// 处理更新用户请求
if (isset($_POST['update_user'])) {
$userId = $_POST['user_id'];
$username = $_POST['username'];
$email = $_POST['email'];
$stmt = $conn->prepare("UPDATE users SET username = ?, email = ? WHERE id = ? AND role = 'developer'");
$stmt->bind_param("ssi", $username, $email, $userId);
$stmt->execute();
$stmt->close();
header("Location: manage_developers.php?updated=true");
exit;
}
// 获取所有开发者用户
$developers = [];
$result = $conn->query("SELECT id, username, email, created_at FROM users WHERE role = 'developer' ORDER BY created_at DESC");
if (!$result) {
error_log('Failed to fetch developers: ' . $conn->error);
die('获取开发者列表失败,请稍后重试');
}
while ($row = $result->fetch_assoc()) {
$developers[] = $row;
}
// 获取要编辑的用户信息
$editUser = null;
if (isset($_GET['edit'])) {
$editUserId = $_GET['edit'];
$stmt = $conn->prepare("SELECT id, username, email FROM users WHERE id = ? AND role = 'developer'");
$stmt->bind_param("i", $editUserId);
$stmt->execute();
$editUser = $stmt->get_result()->fetch_assoc();
$stmt->close();
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理开发者用户 - 应用商店管理</title>
<link rel="stylesheet" href="../styles.css">
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.user-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.user-table th, .user-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.user-table th {
background-color: #f2f2f2;
font-weight: bold;
}
.action-btn {
padding: 6px 12px;
margin: 0 5px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.edit-btn {
background-color: #4CAF50;
color: white;
}
.delete-btn {
background-color: #f44336;
color: white;
}
.edit-form {
background-color: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.submit-btn {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.message {
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
}
.success {
background-color: #dff0d8;
color: #3c763d;
}
</style>
</head>
<body>
<div class="container">
<h1>管理开发者用户</h1>
<?php if (isset($_GET['deleted'])): ?>
<div class="message success">用户已成功删除</div>
<?php endif; ?>
<?php if (isset($_GET['updated'])): ?>
<div class="message success">用户信息已成功更新</div>
<?php endif; ?>
<?php if ($editUser): ?>
<div class="edit-form">
<h2>编辑开发者用户</h2>
<form method="post" action="manage_developers.php">
<input type="hidden" name="user_id" value="<?php echo $editUser['id']; ?>">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" value="<?php echo htmlspecialchars($editUser['username']); ?>" required>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" name="email" value="<?php echo htmlspecialchars($editUser['email']); ?>" required>
</div>
<button type="submit" name="update_user" class="submit-btn">更新用户</button>
<a href="manage_developers.php" class="action-btn">取消</a>
</form>
</div>
<?php endif; ?>
<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($developers as $developer): ?>
<tr>
<td><?php echo $developer['id']; ?></td>
<td><?php echo htmlspecialchars($developer['username']); ?></td>
<td><?php echo htmlspecialchars($developer['email']); ?></td>
<td><?php echo $developer['created_at']; ?></td>
<td>
<a href="manage_developers.php?edit=<?php echo $developer['id']; ?>" class="action-btn edit-btn">编辑</a>
<form method="post" action="manage_developers.php" style="display: inline-block;" onsubmit="return confirm('确定要删除这个用户吗?');">
<input type="hidden" name="user_id" value="<?php echo $developer['id']; ?>">
<button type="submit" name="delete_user" class="action-btn delete-btn">删除</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($developers)): ?>
<tr>
<td colspan="5" style="text-align: center;">暂无开发者用户</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</body>
</html>

208
admin/review_apps.php Normal file
View File

@@ -0,0 +1,208 @@
<?php
require_once '../config.php';
session_start();
// 检查管理员登录状态
if (!isset($_SESSION['admin'])) {
header('Location: login.php');
exit;
}
$success = '';
$error = '';
// 处理审核操作
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['review_action'])) {
$appId = $_POST['app_id'];
$action = $_POST['review_action'];
$rejectionReason = $_POST['rejection_reason'] ?? '';
// 验证应用ID
if (!is_numeric($appId)) {
$error = '无效的应用ID';
} else {
// 检查数据库连接
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
// 更新应用状态
$status = $action === 'approve' ? 'approved' : 'rejected';
$stmt = $conn->prepare("UPDATE apps SET status = ?, rejection_reason = ? WHERE id = ?");
if (!$stmt) {
$error = "数据库错误: " . $conn->error;
} else {
$stmt->bind_param("ssi", $status, $rejectionReason, $appId);
if ($stmt->execute()) {
$success = '应用审核已更新';
} else {
$error = '更新审核状态失败: ' . $conn->error;
}
$stmt->close();
}
}
}
}
// 获取待审核应用列表
$pendingApps = [];
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
$stmt = $conn->prepare("SELECT a.id, a.name, a.description, a.status, a.created_at
FROM apps a
WHERE a.status = 'pending'
ORDER BY a.created_at DESC");
if (!$stmt) {
$error = "数据库错误: " . $conn->error;
} else {
$stmt->execute();
$result = $stmt->get_result();
$pendingApps = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
}
}
?>
<!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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 自定义CSS -->
<link rel="stylesheet" href="../styles.css">
<!-- Fluent Design 模糊效果 -->
<style>
.blur-bg {
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.5);
}
.app-card {
transition: transform 0.2s;
}
.app-card:hover {
transform: scale(1.02);
}
</style>
</head>
<body>
<!-- 导航栏 -->
<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="index.php">App列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="addapp.php">添加App</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="review_apps.php">应用审核</a>
</li>
<li class="nav-item">
<a class="nav-link" href="?logout=true">退出登录</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<?php if (!empty($success)): ?>
<div class="alert alert-success"><?php echo $success; ?></div>
<?php endif; ?>
<?php if (!empty($error)): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<h2>应用审核</h2>
<p class="text-muted">待审核应用: <?php echo count($pendingApps); ?></p>
<?php if (empty($pendingApps)): ?>
<div class="alert alert-info">没有待审核的应用</div>
<?php else: ?>
<div class="row">
<?php foreach ($pendingApps as $app): ?>
<div class="col-md-6 mb-4">
<div class="card app-card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0"><?php echo htmlspecialchars($app['name']); ?></h5>
</div>
<div class="card-body">
<p class="card-text"><strong>开发者:</strong> <?php echo htmlspecialchars($app['username']); ?></p>
<p class="card-text"><strong>提交时间:</strong> <?php echo htmlspecialchars($app['created_at']); ?></p>
<p class="card-text"><strong>描述:</strong> <?php echo nl2br(htmlspecialchars($app['description'])); ?></p>
<!-- 获取应用图片 -->
<?php
$images = [];
$stmt = $conn->prepare("SELECT image_path FROM app_images WHERE app_id = ?");
$stmt->bind_param("i", $app['id']);
$stmt->execute();
$imgResult = $stmt->get_result();
while ($img = $imgResult->fetch_assoc()) {
$images[] = $img['image_path'];
}
$stmt->close();
?>
<?php if (!empty($images)): ?>
<div class="mb-3">
<strong>预览图片:</strong><br>
<img src="<?php echo htmlspecialchars($images[0]); ?>" alt="应用截图" class="img-thumbnail" style="max-width: 200px;">
</div>
<?php endif; ?>
<form method="post" class="mt-3">
<input type="hidden" name="app_id" value="<?php echo $app['id']; ?>">
<div class="d-flex gap-2">
<button type="submit" name="review_action" value="approve" class="btn btn-success flex-grow-1">通过</button>
<button type="button" class="btn btn-danger flex-grow-1" data-bs-toggle="modal" data-bs-target="#rejectModal<?php echo $app['id']; ?>">拒绝</button>
</div>
</form>
</div>
</div>
</div>
<!-- 拒绝原因模态框 -->
<div class="modal fade" id="rejectModal<?php echo $app['id']; ?>" tabindex="-1" aria-labelledby="rejectModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="rejectModalLabel">拒绝应用: <?php echo htmlspecialchars($app['name']); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post">
<div class="modal-body">
<input type="hidden" name="app_id" value="<?php echo $app['id']; ?>">
<div class="mb-3">
<label for="rejection_reason<?php echo $app['id']; ?>" class="form-label">拒绝原因</label>
<textarea class="form-control" id="rejection_reason<?php echo $app['id']; ?>" name="rejection_reason" rows="3" required></textarea>
<div class="form-text">请详细说明拒绝原因,帮助开发者改进应用</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" name="review_action" value="reject" class="btn btn-danger">确认拒绝</button>
</div>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Bootstrap JS with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -11,7 +11,10 @@ CREATE TABLE IF NOT EXISTS apps (
age_rating_description TEXT,
platforms JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
version VARCHAR(20) NOT NULL,
changelog TEXT NOT NULL,
file_path VARCHAR(255) NOT NULL
);
-- 创建APP版本表
@@ -165,6 +168,41 @@ CREATE TABLE IF NOT EXISTS user_favorites (
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
);
-- 创建开发者表
CREATE TABLE IF NOT EXISTS developers (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 修改 apps 表,添加 developer_id 和 status 字段
SET @exist_developer_id = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apps' AND COLUMN_NAME = 'developer_id');
SET @sql = IF(@exist_developer_id = 0, 'ALTER TABLE apps ADD COLUMN developer_id INT', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exist_status = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apps' AND COLUMN_NAME = 'status');
SET @sql = IF(@exist_status = 0, 'ALTER TABLE apps ADD COLUMN status ENUM(''pending'', ''approved'', ''rejected'') DEFAULT ''pending''', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 添加应用拒绝原因字段
SET @exist_rejection_reason = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apps' AND COLUMN_NAME = 'rejection_reason');
SET @sql = IF(@exist_rejection_reason = 0, 'ALTER TABLE apps ADD COLUMN rejection_reason TEXT NULL', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exist_fk = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apps' AND COLUMN_NAME = 'developer_id' AND CONSTRAINT_NAME = 'fk_apps_developers');
SET @sql = IF(@exist_fk = 0, 'ALTER TABLE apps ADD CONSTRAINT fk_apps_developers FOREIGN KEY (developer_id) REFERENCES developers(id) ON DELETE SET NULL', 'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 创建应用推荐表
CREATE TABLE IF NOT EXISTS app_recommendations (
id INT AUTO_INCREMENT PRIMARY KEY,

View File

@@ -15,10 +15,25 @@ define('ADMIN_PASSWORD', '123456');
// 数据库连接
$conn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
if ($conn->connect_error) {
die('数据库连接失败: ' . $conn->connect_error);
$error_msg = '数据库连接失败: ' . $conn->connect_error;
log_error($error_msg, __FILE__, __LINE__);
die($error_msg);
}
$conn->set_charset('utf8mb4');
// 设置时区
date_default_timezone_set('Asia/Shanghai');
// 错误日志记录函数
function log_error($message, $file = '', $line = '') {
$log_entry = date('[Y-m-d H:i:s]') . ' Error: ' . $message;
if (!empty($file)) {
$log_entry .= ' in ' . $file;
}
if (!empty($line)) {
$log_entry .= ' on line ' . $line;
}
$log_entry .= "\n";
file_put_contents('d:\\app2\\logs\\error.log', $log_entry, FILE_APPEND);
}
?>

211
developer/dashboard.php Normal file
View File

@@ -0,0 +1,211 @@
<?php
// 引入配置文件
require_once '../config.php';
session_start();
// 检查开发者是否已登录
if (!isset($_SESSION['developer_id'])) {
header('Location: login.php');
exit;
}
$developerId = $_SESSION['developer_id'];
$developerUsername = $_SESSION['developer_username'];
// 检查数据库连接是否为 MySQLi 对象
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
// 获取开发者的应用列表
$apps = [];
$stmt = $conn->prepare('SELECT id, name, status, rejection_reason FROM apps WHERE developer_id = ?');
if (!$stmt) {
log_error('获取应用列表查询准备失败: ' . $conn->error, __FILE__, __LINE__);
$error = '获取应用列表时发生错误,请稍后再试';
} else {
$stmt->bind_param('i', $developerId);
if (!$stmt->execute()) {
log_error('获取应用列表查询执行失败: ' . $stmt->error, __FILE__, __LINE__);
$error = '获取应用列表时发生错误,请稍后再试';
} else {
$result = $stmt->get_result();
$apps = $result->fetch_all(MYSQLI_ASSOC);
}
}
}
?>
<!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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 自定义CSS -->
<link rel="stylesheet" href="../styles.css">
<style>
.blur-bg {
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.5);
}
.app-card {
margin-bottom: 1rem;
}
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
.dashboard-container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
}
.app-list {
margin-top: 20px;
}
.app-item {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
.status-pending {
color: orange;
}
.status-approved {
color: green;
}
.status-rejected {
color: red;
}
.action-buttons {
margin-top: 10px;
}
.action-buttons a {
display: inline-block;
padding: 5px 10px;
background-color: #007BFF;
color: #fff;
text-decoration: none;
border-radius: 3px;
margin-right: 10px;
}
.action-buttons a:hover {
background-color: #0056b3;
}
.add-app {
margin-bottom: 20px;
}
.add-app a {
display: inline-block;
padding: 10px 20px;
background-color: #28a745;
color: #fff;
text-decoration: none;
border-radius: 3px;
}
.add-app a:hover {
background-color: #218838;
}
.logout {
text-align: right;
}
.logout a {
color: #dc3545;
text-decoration: none;
}
.logout a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<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 active" aria-current="page" href="dashboard.php">应用仪表盘</a>
</li>
<li class="nav-item">
<a class="nav-link" 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="dashboard-container mt-4">
<?php
$rejectedApps = array_filter($apps, function($app) {
return $app['status'] === 'rejected';
});
if (!empty($rejectedApps)):
?>
<div class="alert alert-danger">
<strong>提醒:</strong> 您有 <?php echo count($rejectedApps); ?> 个应用未通过审核,请查看详情。
</div>
<?php endif; ?>
<h1>欢迎,<?php echo htmlspecialchars($developerUsername); ?></h1>
<div class="add-app">
<a href="upload_app.php">上传新应用</a>
</div>
<?php if (isset($error)): ?>
<div style="color: red;"><?php echo $error; ?></div>
<?php endif; ?>
<div class="app-list">
<h2>我的应用</h2>
<?php if (empty($apps)): ?>
<p>您还没有上传任何应用。</p>
<?php else: ?>
<?php foreach ($apps as $app): ?>
<div class="card app-card">
<div class="card-body">
<h5 class="card-title"><?php echo htmlspecialchars($app['name']); ?></h5>
<p class="card-text">
状态:
<?php if ($app['status'] === 'approved'): ?>
<span class="badge bg-success">已通过</span>
<?php elseif ($app['status'] === 'rejected'): ?>
<span class="badge bg-danger">未通过</span>
<div class="alert alert-warning mt-2">
拒绝原因: <?php echo htmlspecialchars($app['rejection_reason']); ?>
</div>
<?php else: ?>
<span class="badge bg-warning">待审核</span>
<?php endif; ?>
</p>
<div class="action-buttons">
<a href="edit_app.php?id=<?php echo $app['id']; ?>" class="btn btn-primary">编辑</a>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- Bootstrap JS and Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

434
developer/edit_app.php Normal file
View File

@@ -0,0 +1,434 @@
<?php
// 引入配置文件
require_once '../config.php';
session_start();
// 检查开发者是否已登录
if (!isset($_SESSION['developer_id'])) {
header('Location: login.php');
exit;
}
$developerId = $_SESSION['developer_id'];
$error = '';
$success = '';
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header('Location: dashboard.php');
exit;
}
$appId = $_GET['id'];
$app = [];
// 检查数据库连接是否为 MySQLi 对象
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
header('Location: dashboard.php');
exit;
}
// 获取所有标签
$tags = [];
$tagStmt = $conn->query('SELECT id, name FROM tags');
while ($tag = $tagStmt->fetch_assoc()) {
$tags[] = $tag;
}
$tagStmt->close();
// 获取应用现有标签
$appTags = [];
$appTagStmt = $conn->prepare('SELECT tag_id FROM app_tags WHERE app_id = ?');
$appTagStmt->bind_param('i', $appId);
$appTagStmt->execute();
$appTagResult = $appTagStmt->get_result();
while ($tag = $appTagResult->fetch_assoc()) {
$appTags[] = $tag['tag_id'];
}
$appTagStmt->close();
// 获取应用信息并验证开发者权限
$stmt = $conn->prepare('SELECT id, name, description, version, changelog, age_rating, age_rating_description, platforms, file_path FROM apps WHERE id = ? AND developer_id = ?');
if (!$stmt) {
log_error('获取应用信息查询准备失败: ' . $conn->error, __FILE__, __LINE__);
$error = '获取应用信息时发生错误,请稍后再试';
header('Location: dashboard.php');
exit;
}
$stmt->bind_param('ii', $appId, $developerId);
if (!$stmt->execute()) {
log_error('获取应用信息查询执行失败: ' . $stmt->error, __FILE__, __LINE__);
$error = '获取应用信息时发生错误,请稍后再试';
header('Location: dashboard.php');
exit;
}
$result = $stmt->get_result();
$app = $result->fetch_assoc();
if (!$app) {
header('Location: dashboard.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$appName = trim($_POST['name']);
$appDescription = trim($_POST['description']);
$version = trim($_POST['version']);
$changelog = trim($_POST['changelog']);
$ageRating = $_POST['age_rating'];
$ageRatingDescription = trim($_POST['age_rating_description']);
$platforms = $_POST['platforms'] ?? [];
$platforms_json = json_encode($platforms);
$appFilePath = $app['file_path']; // 默认使用现有文件路径
// 处理应用文件上传
if (!empty($_FILES['app_file']['tmp_name'])) {
$uploadDir = '../uploads/apps/';
$fileExtension = pathinfo($_FILES['app_file']['name'], PATHINFO_EXTENSION);
$newFileName = uniqid() . '.' . $fileExtension;
$targetPath = $uploadDir . $newFileName;
// 验证文件类型和大小
$allowedTypes = ['apk', 'exe', 'jar', 'crx', 'ini'];
if (!in_array($fileExtension, $allowedTypes)) {
$error = '不支持的文件类型,请上传 ' . implode(', ', $allowedTypes) . ' 格式的文件';
} elseif ($_FILES['app_file']['size'] > 50 * 1024 * 1024) { // 50MB
$error = '文件大小不能超过50MB';
} elseif (!move_uploaded_file($_FILES['app_file']['tmp_name'], $targetPath)) {
$error = '文件上传失败,请检查服务器权限';
} else {
// 删除旧文件
if (file_exists($appFilePath)) {
unlink($appFilePath);
}
$appFilePath = $targetPath;
}
}
// 处理图片删除
if (!empty($_POST['removed_images'])) {
$removedImageIds = explode(',', $_POST['removed_images']);
foreach ($removedImageIds as $imgId) {
if (is_numeric($imgId)) {
// 获取图片路径
$stmt = $conn->prepare("SELECT image_path FROM app_images WHERE id = ?");
$stmt->bind_param("i", $imgId);
$stmt->execute();
$imgResult = $stmt->get_result();
if ($img = $imgResult->fetch_assoc()) {
// 删除文件
if (file_exists($img['image_path'])) {
unlink($img['image_path']);
}
// 删除数据库记录
$deleteStmt = $conn->prepare("DELETE FROM app_images WHERE id = ?");
$deleteStmt->bind_param("i", $imgId);
$deleteStmt->execute();
$deleteStmt->close();
}
$stmt->close();
}
}
}
// 更新应用标签
// 删除现有标签关联
$deleteTagStmt = $conn->prepare('DELETE FROM app_tags WHERE app_id = ?');
$deleteTagStmt->bind_param('i', $appId);
$deleteTagStmt->execute();
$deleteTagStmt->close();
// 添加新标签关联
foreach ($selectedTags as $tagId) {
if (is_numeric($tagId)) {
$tagStmt = $conn->prepare('INSERT INTO app_tags (app_id, tag_id) VALUES (?, ?)');
$tagStmt->bind_param('ii', $appId, $tagId);
$tagStmt->execute();
$tagStmt->close();
}
}
// 处理新图片上传
$imageUploadDir = '../uploads/images/';
$allowedImageTypes = ['jpg', 'jpeg', 'png'];
$maxImages = 5;
$currentImageCount = count($existingImages) - count($removedImageIds ?? []);
if (!empty($_FILES['images']['name'][0]) && empty($error)) {
$newImages = $_FILES['images'];
for ($i = 0; $i < count($newImages['name']); $i++) {
if ($newImages['error'][$i] !== UPLOAD_ERR_OK) continue;
$fileName = $newImages['name'][$i];
$fileTmp = $newImages['tmp_name'][$i];
$fileSize = $newImages['size'][$i];
$fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($fileExt, $allowedImageTypes)) {
$error = "图片 {$fileName} 格式不支持仅允许jpg、png";
break;
}
if ($fileSize > 2 * 1024 * 1024) { // 2MB
$error = "图片 {$fileName} 大小超过2MB";
break;
}
if ($currentImageCount >= $maxImages) {
$error = "最多只能上传5张图片";
break;
}
$newFileName = uniqid() . '.' . $fileExt;
$targetPath = $imageUploadDir . $newFileName;
if (move_uploaded_file($fileTmp, $targetPath)) {
// 插入数据库
$stmt = $conn->prepare("INSERT INTO app_images (app_id, image_path) VALUES (?, ?)");
$stmt->bind_param("is", $appId, $targetPath);
$stmt->execute();
$stmt->close();
$currentImageCount++;
} else {
$error = "图片 {$fileName} 上传失败";
break;
}
}
}
// 验证标签选择
$selectedTags = $_POST['tags'] ?? [];
if (empty($selectedTags)) {
$error = '至少需要选择一个应用标签';
}
if (empty($appName) || empty($appDescription) || empty($version) || empty($changelog) || empty($ageRating) || empty($ageRatingDescription)) {
$error = '应用名称和描述不能为空';
} else {
// 检查数据库连接是否为 MySQLi 对象
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
$platforms = $_POST['platforms'] ?? [];
$platforms_json = json_encode($platforms);
$stmt = $conn->prepare('UPDATE apps SET name = ?, description = ?, version = ?, changelog = ?, age_rating = ?, age_rating_description = ?, platforms = ?, file_path = ?, status = \'pending\' WHERE id = ? AND developer_id = ?');
if (!$stmt) {
log_error('更新应用信息查询准备失败: ' . $conn->error, __FILE__, __LINE__);
$error = '更新应用信息时发生错误,请稍后再试';
} else {
$stmt->bind_param('ssssssssii', $appName, $appDescription, $version, $changelog, $ageRating, $ageRatingDescription, $platforms_json, $appFilePath, $appId, $developerId);
if (!$stmt->execute()) {
log_error('更新应用信息查询执行失败: ' . $stmt->error, __FILE__, __LINE__);
$error = '更新应用信息时发生错误,请稍后再试';
} else {
$success = '应用信息更新成功,请等待管理员重新审核';
header('Location: dashboard.php?success=' . urlencode($success));
exit;
$app['name'] = $appName;
$app['description'] = $appDescription;
}
}
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>编辑应用</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 500px;
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 3px;
resize: vertical;
}
input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 3px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #007BFF;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
.error {
color: red;
margin-bottom: 10px;
}
.success {
color: green;
margin-bottom: 10px;
}
.back-link {
text-align: center;
margin-top: 10px;
}
.back-link a {
color: #007BFF;
text-decoration: none;
}
.back-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h2>编辑应用</h2>
<?php if (!empty($error)): ?>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="success"><?php echo $success; ?></div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="name">应用名称</label>
<input type="text" id="name" name="name" value="<?php echo htmlspecialchars($app['name']); ?>" required>
</div>
<div class="form-group">
<label for="description">应用描述</label>
<textarea id="description" name="description" rows="5" required><?php echo htmlspecialchars($app['description']); ?></textarea>
</div>
<div class="form-group">
<label for="version">版本号</label>
<input type="text" id="version" name="version" value="<?php echo htmlspecialchars($app['version']); ?>" required>
</div>
<div class="form-group">
<label for="changelog">更新日志</label>
<textarea id="changelog" name="changelog" rows="3" required><?php echo htmlspecialchars($app['changelog']); ?></textarea>
</div>
<div class="form-group">
<label for="age_rating">年龄分级</label>
<select id="age_rating" name="age_rating" required>
<option value="3+" <?php echo $app['age_rating'] === '3+' ? 'selected' : ''; ?>>3+</option>
<option value="7+" <?php echo $app['age_rating'] === '7+' ? 'selected' : ''; ?>>7+</option>
<option value="12+" <?php echo $app['age_rating'] === '12+' ? 'selected' : ''; ?>>12+</option>
<option value="17+" <?php echo $app['age_rating'] === '17+' ? 'selected' : ''; ?>>17+</option>
</select>
</div>
<div class="form-group">
<label for="age_rating_description">年龄分级说明</label>
<input type="text" id="age_rating_description" name="age_rating_description" value="<?php echo htmlspecialchars($app['age_rating_description']); ?>" required>
</div>
<div class="form-group">
<label>适用平台</label>
<?php $platforms = json_decode($app['platforms'], true) ?? []; ?>
<div>
<input type="checkbox" id="platform_android" name="platforms[]" value="Android" <?php echo in_array('Android', $platforms) ? 'checked' : ''; ?>>
<label for="platform_android">Android</label>
</div>
<div>
<input type="checkbox" id="platform_ios" name="platforms[]" value="iOS" <?php echo in_array('iOS', $platforms) ? 'checked' : ''; ?>>
<label for="platform_ios">iOS</label>
</div>
</div>
<div class="form-group">
<label>应用标签 (至少选择1个)</label>
<div class="d-flex flex-wrap gap-3">
<?php foreach ($tags as $tag): ?>
<div>
<input type="checkbox" id="tag_<?php echo $tag['id']; ?>" name="tags[]" value="<?php echo $tag['id']; ?>" <?php echo in_array($tag['id'], $appTags) ? 'checked' : ''; ?>>
<label for="tag_<?php echo $tag['id']; ?>"><?php echo htmlspecialchars($tag['name']); ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="form-group">
<label for="app_file">更新应用文件</label>
<input type="file" id="app_file" name="app_file">
<small>当前文件: <?php echo basename($app['file_path']); ?></small>
</div>
<div class="form-group">
<label>应用图片 (最多5张)</label>
<?php
// 获取现有图片
$existingImages = [];
$stmt = $conn->prepare("SELECT id, image_path FROM app_images WHERE app_id = ?");
$stmt->bind_param("i", $appId);
$stmt->execute();
$imgResult = $stmt->get_result();
while ($img = $imgResult->fetch_assoc()) {
$existingImages[] = $img;
}
$stmt->close();
?>
<!-- 现有图片 -->
<?php if (!empty($existingImages)): ?>
<div class="mb-3">
<label>现有图片:</label>
<div class="d-flex flex-wrap gap-2">
<?php foreach ($existingImages as $img): ?>
<div class="position-relative">
<img src="<?php echo htmlspecialchars($img['image_path']); ?>" alt="应用图片" style="width: 100px; height: 100px; object-fit: cover; border-radius: 4px;">
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0" onclick="removeImage(<?php echo $img['id']; ?>)">×</button>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- 新图片上传 -->
<input type="file" name="images[]" multiple accept="image/*" class="form-control">
<small>支持jpg、png格式最多上传5张图片</small>
</div>
<input type="hidden" name="removed_images" id="removed_images" value="">
<input type="submit" value="保存更改">
</form>
<div class="back-link">
<a href="dashboard.php">返回仪表盘</a>
</div>
</div>
<script>
function removeImage(imageId) {
const removedInput = document.getElementById('removed_images');
const currentValues = removedInput.value ? removedInput.value.split(',') : [];
if (!currentValues.includes(imageId.toString())) {
currentValues.push(imageId);
removedInput.value = currentValues.join(',');
}
// 从DOM中移除图片元素
event.target.closest('.position-relative').remove();
}
</script>
</body>
</html>

142
developer/login.php Normal file
View File

@@ -0,0 +1,142 @@
<?php
// 引入配置文件
require_once '../config.php';
session_start();
$error = '';
if (isset($_GET['register_success']) && $_GET['register_success'] == 1) {
$success = '注册成功,请登录';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email']);
$password = $_POST['password'];
if (empty($email) || empty($password)) {
$error = '邮箱和密码不能为空';
} else {
// 检查数据库连接是否为 MySQLi 对象
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
$stmt = $conn->prepare('SELECT id, username, password FROM developers WHERE email = ?');
if (!$stmt) {
log_error('登录查询准备失败: ' . $conn->error, __FILE__, __LINE__);
$error = '登录时发生错误,请稍后再试';
} else {
$stmt->bind_param('s', $email);
if (!$stmt->execute()) {
log_error('登录查询执行失败: ' . $stmt->error, __FILE__, __LINE__);
$error = '登录时发生错误,请稍后再试';
} else {
$result = $stmt->get_result();
$developer = $result->fetch_assoc();
if ($developer && password_verify($password, $developer['password'])) {
$_SESSION['developer_id'] = $developer['id'];
$_SESSION['developer_username'] = $developer['username'];
header('Location: dashboard.php');
exit;
} else {
$error = '邮箱或密码错误';
}
}
}
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者登录</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 3px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #007BFF;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
.error {
color: red;
margin-bottom: 10px;
}
.success {
color: green;
margin-bottom: 10px;
}
.register-link {
text-align: center;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h2>开发者登录</h2>
<?php if (isset($success)): ?>
<div class="success"><?php echo $success; ?></div>
<?php endif; ?>
<?php if (!empty($error)): ?>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<form method="post">
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="登录">
</form>
<div class="register-link">
还没有账号?<a href="register.php">注册</a>
</div>
</div>
</body>
</html>

10
developer/logout.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
session_start();
// 销毁会话数据
session_unset();
session_destroy();
// 重定向到登录页面
header('Location: login.php');
exit;

136
developer/register.php Normal file
View File

@@ -0,0 +1,136 @@
<?php
// 引入配置文件
require_once '../config.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username']);
$email = trim($_POST['email']);
$password = $_POST['password'];
if (empty($username) || empty($email) || empty($password)) {
$error = '用户名、邮箱和密码不能为空';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = '请输入有效的邮箱地址';
} else {
// 检查数据库连接是否为 PDO 对象
if (!($conn instanceof mysqli)) {
log_error('数据库连接错误: 连接不是MySQLi实例', __FILE__, __LINE__);
$error = '数据库连接错误,请检查配置';
} else {
try {
$stmt = $conn->prepare('SELECT id FROM developers WHERE username = ? OR email = ?');
$stmt->bind_param('ss', $username, $email);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$error = '用户名或邮箱已被注册';
} else {
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$insertStmt = $conn->prepare('INSERT INTO developers (username, email, password) VALUES (?, ?, ?)');
if (!$insertStmt) {
log_error('插入准备失败: ' . $conn->error, __FILE__, __LINE__);
$error = '系统错误,请稍后再试';
} else {
$insertStmt->bind_param('sss', $username, $email, $hashedPassword);
if (!$insertStmt->execute()) {
log_error('插入执行失败: ' . $insertStmt->error, __FILE__, __LINE__);
$error = '系统错误,请稍后再试';
}
}
header('Location: login.php?register_success=1');
exit;
}
} catch (PDOException $e) {
$error = '注册时发生错误,请稍后再试';
}
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者注册</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 3px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #007BFF;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
.error {
color: red;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<h2>开发者注册</h2>
<?php if (!empty($error)): ?>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<form method="post">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="注册">
</form>
</div>
</body>
</html>

470
developer/upload_app.php Normal file
View File

@@ -0,0 +1,470 @@
<?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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 自定义CSS -->
<link rel="stylesheet" href="../styles.css">
<!-- 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限制
alert('文件大小不能超过100MB');
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限制
alert(`图片 ${file.name} 大小不能超过10MB`);
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>

View File

@@ -41,9 +41,22 @@ if (!isset($conn) || !$conn instanceof mysqli) {
<a class="nav-link" href="/admin/">管理</a>
</li>
<?php endif; ?>
</li>
<li class="nav-item">
<a class="nav-link" href="tags.php">标签</a>
</li>
<?php if (isset($_SESSION['developer_id'])): ?>
<li class="nav-item">
<a class="nav-link" href="developer/dashboard.php">进入面板</a>
</li>
<?php else: ?>
<li class="nav-item">
<a class="nav-link" href="developer/register.php">开发者注册</a>
</li>
<li class="nav-item">
<a class="nav-link" href="developer/login.php">开发者登录</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
@@ -134,6 +147,9 @@ if (!isset($conn) || !$conn instanceof mysqli) {
$paramTypes .= 'ss';
}
// 只显示已审核通过的应用
$conditions[] = "apps.status = 'approved'";
// 添加条件
if (!empty($conditions)) {
$sql .= "WHERE " . implode(" AND ", $conditions);