feat: 实现完整的应用商店系统
- 添加前端页面包括首页、应用详情页和版本历史页 - 实现管理员后台功能,包括应用管理、版本管理和登录验证 - 添加API接口用于获取应用列表和详情 - 实现文件上传和下载功能 - 添加Windows控制台和GUI客户端 - 完善数据库结构和初始化脚本 - 添加样式表和图片资源
This commit is contained in:
4
.htaccess
Normal file
4
.htaccess
Normal file
@@ -0,0 +1,4 @@
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^api(.*)$ api.php$1 [L]
|
||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# App Store 项目
|
||||
|
||||
这是一个基于 PHP 7.4 的 App Store 项目,使用 Bootstrap 实现 Fluent Design 风格界面,数据存储采用 MySQL 数据库。
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
app2/
|
||||
├── config.php # 配置文件,包含数据库和管理员信息
|
||||
├── app_store.sql # 数据库初始化 SQL 文件
|
||||
├── index.php # 首页
|
||||
├── app.php # App 信息页
|
||||
├── admin.php # App 管理页
|
||||
├── api.php # API 接口文件
|
||||
├── styles.css # 自定义 CSS 文件
|
||||
├── images/ # 存储 App 预览图片和年龄分级 SVG
|
||||
│ ├── age_3plus.svg
|
||||
│ ├── age_7plus.svg
|
||||
│ ├── age_12plus.svg
|
||||
│ ├── age_17plus.svg
|
||||
├── files/ # 存储 App 文件
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
- PHP 7.4
|
||||
- MySQL
|
||||
- Web 服务器(如 Apache 或 Nginx)
|
||||
|
||||
## 安装步骤
|
||||
1. 创建项目目录并将代码复制到该目录下。
|
||||
2. 修改 `config.php` 文件,配置 MySQL 数据库信息和管理员账号。
|
||||
3. 执行 `app_store.sql` 文件,创建数据库和表结构。可以使用以下命令:
|
||||
```sql
|
||||
mysql -u your_username -p your_database < app_store.sql
|
||||
```
|
||||
4. 创建 `files` 和 `images` 目录,并确保 Web 服务器对这些目录有写入权限。
|
||||
|
||||
## 功能说明
|
||||
- **首页**:展示最新 App 列表,包含基本信息和评分。
|
||||
- **App 信息页**:显示 App 详细信息、版本历史、预览图片和用户评价,支持用户评分。
|
||||
- **管理页**:管理员可以添加、删除 App,上传 App 文件和预览图片。
|
||||
- **API 接口**:提供 `/api` 获取 App 列表,`/api/app/<编号>` 获取单个 App 详细信息。
|
||||
|
||||
## 管理员登录
|
||||
默认管理员账号信息在 `config.php` 中配置,登录后可访问管理页面。
|
||||
|
||||
## 注意事项
|
||||
- 请确保 `files` 和 `images` 目录有足够的写入权限。
|
||||
- 生产环境中建议修改管理员密码和数据库信息,保证系统安全。
|
||||
313
admin/addapp.php
Normal file
313
admin/addapp.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
|
||||
session_start();
|
||||
// 检查管理员登录状态
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
// 处理添加App请求
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_app'])) {
|
||||
$name = $_POST['name'];
|
||||
$description = $_POST['description'];
|
||||
$ageRating = $_POST['age_rating'];
|
||||
$platforms = isset($_POST['platforms']) ? json_encode($_POST['platforms']) : json_encode([]);
|
||||
|
||||
// 处理表单提交
|
||||
// 验证必填字段
|
||||
$required = ['name', 'description', 'age_rating', 'platforms'];
|
||||
$errors = [];
|
||||
foreach ($required as $field) {
|
||||
if (empty($_POST[$field])) {
|
||||
$errors[] = ucfirst($field) . ' 不能为空';
|
||||
}
|
||||
}
|
||||
|
||||
// 年龄分级说明验证
|
||||
if (($_POST['age_rating'] === '12+' || $_POST['age_rating'] === '17+') && empty($_POST['age_rating_description'])) {
|
||||
$errors[] = '年龄分级为12+或以上时,年龄分级说明不能为空';
|
||||
}
|
||||
|
||||
|
||||
// 处理应用图标上传
|
||||
|
||||
// 处理平台数据
|
||||
$platforms = json_encode($_POST['platforms']);
|
||||
// 插入应用数据
|
||||
$stmt = $conn->prepare("INSERT INTO apps (name, description, age_rating, age_rating_description, platforms) VALUES (?, ?, ?, ?, ?)");
|
||||
if (!$stmt) {
|
||||
$error = "Database error: " . $conn->error;
|
||||
}
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("sssss", $name, $description, $ageRating, $_POST['age_rating_description'], $platforms);
|
||||
if ($stmt->execute() === TRUE) {
|
||||
$appId = $stmt->insert_id;
|
||||
|
||||
// 处理上传的预览图片
|
||||
if (!empty($_FILES['images']['name'][0])) {
|
||||
$uploadDir = '../images/';
|
||||
foreach ($_FILES['images']['tmp_name'] as $key => $tmpName) {
|
||||
$fileName = basename($_FILES['images']['name'][$key]);
|
||||
$targetPath = $uploadDir . $fileName;
|
||||
if (move_uploaded_file($tmpName, $targetPath)) {
|
||||
$insertImageSql = "INSERT INTO app_images (app_id, image_path) VALUES (?, ?)";
|
||||
$imgStmt = $conn->prepare($insertImageSql);
|
||||
$imgStmt->bind_param("is", $appId, $targetPath);
|
||||
$imgStmt->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传的App文件
|
||||
if (!empty($_FILES['app_file']['name'])) {
|
||||
$uploadDir = '../files/';
|
||||
$fileName = basename($_FILES['app_file']['name']);
|
||||
$targetPath = $uploadDir . $fileName;
|
||||
if (move_uploaded_file($_FILES['app_file']['tmp_name'], $targetPath)) {
|
||||
$version = $_POST['version'];
|
||||
$changelog = $_POST['changelog'];
|
||||
$insertVersionSql = "INSERT INTO app_versions (app_id, version, changelog, file_path) VALUES (?, ?, ?, ?)";
|
||||
$verStmt = $conn->prepare($insertVersionSql);
|
||||
$verStmt->bind_param("isss", $appId, $version, $changelog, $targetPath);
|
||||
$verStmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: index.php?success=App 添加成功');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'App 添加失败: '. $conn->error;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>添加App - <?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);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php if (isset($error)): ?>
|
||||
<div style='color: red; padding: 10px; background-color: #ffeeee; border-radius: 5px; margin-bottom: 20px;'>
|
||||
<?php echo htmlspecialchars($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- 导航栏 -->
|
||||
<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 active" aria-current="page" href="addapp.php">添加App</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>添加App</h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">App名称</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">描述</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="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="mb-3" id="ageRatingDescriptionGroup" style="display: none;">
|
||||
<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>
|
||||
<div class="form-text">当年龄分级为12+或以上时,此项为必填</div>
|
||||
</div>
|
||||
|
||||
<div class="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="mb-3">
|
||||
<label for="version" class="form-label">版本号</label>
|
||||
<input type="text" class="form-control" id="version" name="version" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="changelog" class="form-label">更新日志</label>
|
||||
<textarea class="form-control" id="changelog" name="changelog" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="app_file" class="form-label">App文件</label>
|
||||
<input class="form-control" type="file" id="app_file" name="app_file" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="images" class="form-label">预览图片 (可多选)</label>
|
||||
<input class="form-control" type="file" id="images" name="images[]" multiple>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" name="add_app">添加App</button>
|
||||
<a href="index.php" class="btn btn-secondary ms-2">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 年龄分级说明显示控制
|
||||
const ageRatingSelect = document.getElementById('age_rating');
|
||||
const descriptionGroup = document.getElementById('ageRatingDescriptionGroup');
|
||||
const descriptionInput = document.getElementById('age_rating_description');
|
||||
|
||||
function toggleAgeDescription() {
|
||||
const selectedRating = ageRatingSelect.value;
|
||||
if (selectedRating === '12+' || selectedRating === '17+') {
|
||||
descriptionGroup.style.display = 'block';
|
||||
descriptionInput.required = true;
|
||||
} else {
|
||||
descriptionGroup.style.display = 'none';
|
||||
descriptionInput.required = false;
|
||||
}
|
||||
}
|
||||
|
||||
ageRatingSelect.addEventListener('change', toggleAgeDescription);
|
||||
// 初始加载时检查
|
||||
toggleAgeDescription();
|
||||
|
||||
|
||||
// 导航栏滚动效果
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
|
||||
// 平台子选项显示控制
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交验证
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
// 验证Windows子选项
|
||||
if (document.getElementById('windows').checked && !document.querySelector('input[name="windows_version"]:checked')) {
|
||||
e.preventDefault();
|
||||
alert('请选择Windows版本(XP以前或Win7以后)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证Linux子选项
|
||||
if (document.getElementById('linux').checked && !document.querySelector('input[name="linux_distribution"]:checked')) {
|
||||
e.preventDefault();
|
||||
alert('请选择Linux发行版(Ubuntu、Arch Linux或CentOS)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新平台值包含子选项信息
|
||||
if (document.getElementById('windows').checked) {
|
||||
document.getElementById('windows').value = document.querySelector('input[name="windows_version"]:checked').value;
|
||||
}
|
||||
if (document.getElementById('linux').checked) {
|
||||
document.getElementById('linux').value = document.querySelector('input[name="linux_distribution"]:checked').value;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
41
admin/deleteapp.php
Normal file
41
admin/deleteapp.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
|
||||
session_start();
|
||||
// 检查管理员登录状态
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 验证App ID
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: index.php?error=无效的App ID');
|
||||
exit;
|
||||
}
|
||||
$appId = $_GET['id'];
|
||||
|
||||
// 删除App
|
||||
$deleteAppSql = "DELETE FROM apps WHERE id = ?";
|
||||
$stmt = $conn->prepare($deleteAppSql);
|
||||
$stmt->bind_param("i", $appId);
|
||||
|
||||
if ($stmt->execute() === TRUE) {
|
||||
// 删除关联的图片
|
||||
$deleteImagesSql = "DELETE FROM app_images WHERE app_id = ?";
|
||||
$imgStmt = $conn->prepare($deleteImagesSql);
|
||||
$imgStmt->bind_param("i", $appId);
|
||||
$imgStmt->execute();
|
||||
|
||||
// 删除关联的版本
|
||||
$deleteVersionsSql = "DELETE FROM app_versions WHERE app_id = ?";
|
||||
$verStmt = $conn->prepare($deleteVersionsSql);
|
||||
$verStmt->bind_param("i", $appId);
|
||||
$verStmt->execute();
|
||||
|
||||
header('Location: index.php?success=App 删除成功');
|
||||
} else {
|
||||
header('Location: index.php?error=App 删除失败: '. $conn->error);
|
||||
}
|
||||
exit;
|
||||
?>
|
||||
347
admin/editapp.php
Normal file
347
admin/editapp.php
Normal file
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
|
||||
session_start();
|
||||
// 检查管理员登录状态
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 验证App ID
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: index.php?error=无效的App ID');
|
||||
exit;
|
||||
}
|
||||
$appId = $_GET['id'];
|
||||
|
||||
// 获取App信息
|
||||
$app = null;
|
||||
$getAppSql = "SELECT * FROM apps WHERE id = ?";
|
||||
$stmt = $conn->prepare($getAppSql);
|
||||
$stmt->bind_param("i", $appId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($result->num_rows === 0) {
|
||||
header('Location: index.php?error=App不存在');
|
||||
exit;
|
||||
}
|
||||
$app = $result->fetch_assoc();
|
||||
$platforms = json_decode($app['platforms'], true);
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
// 处理编辑App请求
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_app'])) {
|
||||
$name = $_POST['name'];
|
||||
$description = $_POST['description'];
|
||||
$ageRating = $_POST['age_rating'];
|
||||
$newPlatforms = json_encode($_POST['platforms'] ?? []);
|
||||
|
||||
// 处理表单提交
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 验证必填字段
|
||||
$required = ['name', 'description', 'age_rating', 'platforms'];
|
||||
$errors = [];
|
||||
foreach ($required as $field) {
|
||||
if (empty($_POST[$field])) {
|
||||
$errors[] = ucfirst($field) . ' 不能为空';
|
||||
}
|
||||
}
|
||||
|
||||
// 年龄分级验证
|
||||
if (($_POST['age_rating'] === '12+' || $_POST['age_rating'] === '17+') && empty($_POST['age_rating_description'])) {
|
||||
$errors[] = '年龄分级为12+或以上时,年龄分级说明不能为空';
|
||||
}
|
||||
|
||||
// 处理应用图标上传(如果有新上传)
|
||||
if (!empty($_FILES['images']['name'][0])) {
|
||||
$uploadDir = '../images/';
|
||||
foreach ($_FILES['images']['tmp_name'] as $key => $tmpName) {
|
||||
$fileName = basename($_FILES['images']['name'][$key]);
|
||||
$targetPath = $uploadDir . $fileName;
|
||||
if (move_uploaded_file($tmpName, $targetPath)) {
|
||||
$insertImageSql = "INSERT INTO app_images (app_id, image_path) VALUES (?, ?)";
|
||||
$imgStmt = $conn->prepare($insertImageSql);
|
||||
$imgStmt->bind_param("is", $appId, $targetPath);
|
||||
$imgStmt->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理新上传的App文件
|
||||
if (!empty($_FILES['app_file']['name'])) {
|
||||
$uploadDir = '../files/';
|
||||
$fileName = basename($_FILES['app_file']['name']);
|
||||
$targetPath = $uploadDir . $fileName;
|
||||
if (move_uploaded_file($_FILES['app_file']['tmp_name'], $targetPath)) {
|
||||
$version = $_POST['version'];
|
||||
$changelog = $_POST['changelog'];
|
||||
$insertVersionSql = "INSERT INTO app_versions (app_id, version, changelog, file_path) VALUES (?, ?, ?, ?)";
|
||||
$verStmt = $conn->prepare($insertVersionSql);
|
||||
$verStmt->bind_param("isss", $appId, $version, $changelog, $targetPath);
|
||||
$verStmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: index.php?success=App 更新成功');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'App 更新失败: '. $conn->error;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>编辑App - <?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);
|
||||
}
|
||||
</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="editapp.php?id=<?php echo $appId; ?>">编辑App</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>编辑App: <?php echo htmlspecialchars($app['name']); ?></h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">App名称</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($app['name']); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">描述</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" required><?php echo htmlspecialchars($app['description']); ?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="age_rating" class="form-label">年龄分级</label>
|
||||
<select class="form-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="mb-3" id="ageRatingDescriptionGroup" style="display: none;">
|
||||
<label for="age_rating_description" class="form-label">年龄分级说明</label>
|
||||
<textarea class="form-control" id="age_rating_description" name="age_rating_description" rows="3" placeholder="请说明为何需要此年龄分级"><?php echo htmlspecialchars($app['age_rating_description'] ?? ''); ?></textarea>
|
||||
<div class="form-text">当年龄分级为12+或以上时,此项为必填</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">适用平台</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="android" id="android" name="platforms[]" <?php echo in_array('android', $platforms) ? 'checked' : ''; ?>>
|
||||
<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[]" <?php echo in_array('ios', $platforms) ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="ios">iOS</label>
|
||||
</div>
|
||||
<?php
|
||||
$windowsChecked = false;
|
||||
$windowsVersion = '';
|
||||
foreach ($platforms as $p) {
|
||||
if (strpos($p, 'windows_') === 0) {
|
||||
$windowsChecked = true;
|
||||
$windowsVersion = $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="windows" id="windows" name="platforms[]" <?php echo $windowsChecked ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="windows">Windows</label>
|
||||
</div>
|
||||
<div id="windows_suboptions" class="ms-4 mt-2" style="display: <?php echo $windowsChecked ? 'block' : 'none'; ?>">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="windows_version" id="windows_xp" value="windows_xp" <?php echo $windowsVersion === 'windows_xp' ? 'checked' : ''; ?>>
|
||||
<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" <?php echo $windowsVersion === 'windows_win7' ? 'checked' : ''; ?>>
|
||||
<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[]" <?php echo in_array('macos', $platforms) ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="macos">macOS</label>
|
||||
</div>
|
||||
<?php
|
||||
$linuxChecked = false;
|
||||
$linuxVersion = '';
|
||||
foreach ($platforms as $p) {
|
||||
if (strpos($p, 'linux_') === 0) {
|
||||
$linuxChecked = true;
|
||||
$linuxVersion = $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="linux" id="linux" name="platforms[]" <?php echo $linuxChecked ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="linux">Linux</label>
|
||||
</div>
|
||||
<div id="linux_suboptions" class="ms-4 mt-2" style="display: <?php echo $linuxChecked ? 'block' : 'none'; ?>">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="linux_distribution" id="linux_ubuntu" value="linux_ubuntu" <?php echo $linuxVersion === 'linux_ubuntu' ? 'checked' : ''; ?>>
|
||||
<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" <?php echo $linuxVersion === 'linux_arch' ? 'checked' : ''; ?>>
|
||||
<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" <?php echo $linuxVersion === 'linux_centos' ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="linux_centos">CentOS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="version" class="form-label">新版本号</label>
|
||||
<input type="text" class="form-control" id="version" name="version" placeholder="如: 1.0.1">
|
||||
<div class="form-text">仅在上传新安装包时填写</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="changelog" class="form-label">更新日志</label>
|
||||
<textarea class="form-control" id="changelog" name="changelog" rows="3" placeholder="描述本次更新内容"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="app_file" class="form-label">新App文件 (可选)</label>
|
||||
<input class="form-control" type="file" id="app_file" name="app_file">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="images" class="form-label">新增预览图片 (可选, 可多选)</label>
|
||||
<input class="form-control" type="file" id="images" name="images[]" multiple>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" name="edit_app">更新App</button>
|
||||
<a href="index.php" class="btn btn-secondary ms-2">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 年龄分级说明显示控制
|
||||
// 年龄分级说明显示控制
|
||||
const ageRatingSelect = document.getElementById('age_rating');
|
||||
const descriptionGroup = document.getElementById('ageRatingDescriptionGroup');
|
||||
const descriptionInput = document.getElementById('age_rating_description');
|
||||
|
||||
function toggleAgeDescription() {
|
||||
const selectedRating = ageRatingSelect.value;
|
||||
if (selectedRating === '12+' || selectedRating === '17+') {
|
||||
descriptionGroup.style.display = 'block';
|
||||
descriptionInput.required = true;
|
||||
} else {
|
||||
descriptionGroup.style.display = 'none';
|
||||
descriptionInput.required = false;
|
||||
}
|
||||
}
|
||||
|
||||
ageRatingSelect.addEventListener('change', toggleAgeDescription);
|
||||
// 初始加载时检查
|
||||
toggleAgeDescription();
|
||||
|
||||
// 导航栏滚动效果
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
|
||||
// 平台子选项显示控制
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交验证
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
// 验证Windows子选项
|
||||
if (document.getElementById('windows').checked && !document.querySelector('input[name="windows_version"]:checked')) {
|
||||
e.preventDefault();
|
||||
alert('请选择Windows版本(XP以前或Win7以后)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证Linux子选项
|
||||
if (document.getElementById('linux').checked && !document.querySelector('input[name="linux_distribution"]:checked')) {
|
||||
e.preventDefault();
|
||||
alert('请选择Linux发行版(Ubuntu、Arch Linux或CentOS)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新平台值包含子选项信息
|
||||
if (document.getElementById('windows').checked) {
|
||||
document.getElementById('windows').value = document.querySelector('input[name="windows_version"]:checked').value;
|
||||
}
|
||||
if (document.getElementById('linux').checked) {
|
||||
document.getElementById('linux').value = document.querySelector('input[name="linux_distribution"]:checked').value;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php
|
||||
// 更新应用数据
|
||||
$stmt = $conn->prepare("UPDATE apps SET name=?, description=?, age_rating=?, age_rating_description=?, platforms=?, updated_at=NOW() WHERE id=?");
|
||||
$stmt->bind_param("sssssi", $name, $description, $age_rating, $_POST['age_rating_description'], $platformsJson, $appId);
|
||||
|
||||
// ... existing code ...
|
||||
?>
|
||||
123
admin/index.php
Normal file
123
admin/index.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
|
||||
session_start();
|
||||
// 检查管理员登录状态
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 处理退出登录
|
||||
if (isset($_GET['logout'])) {
|
||||
unset($_SESSION['admin']);
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 获取App列表
|
||||
$sqlApps = "SELECT * FROM apps ORDER BY created_at DESC";
|
||||
$resultApps = $conn->query($sqlApps);
|
||||
|
||||
if (!$resultApps) {
|
||||
error_log("Database query failed: " . $conn->error);
|
||||
echo '<div class="alert alert-danger">获取App列表失败,请联系管理员。</div>';
|
||||
} else {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>App管理 - <?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);
|
||||
}
|
||||
</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="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" href="?logout=true">退出登录</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="alert alert-success"><?php echo $_GET['success']; ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_GET['error'])): ?>
|
||||
<div class="alert alert-danger"><?php echo $_GET['error']; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2>App列表</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>年龄分级</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ($app = $resultApps->fetch_assoc()): ?>
|
||||
<tr>
|
||||
<td><?php echo $app['id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($app['name']); ?></td>
|
||||
<td><?php echo $app['age_rating']; ?></td>
|
||||
<td><?php echo $app['created_at']; ?></td>
|
||||
<td>
|
||||
<a href="editapp.php?id=<?php echo $app['id']; ?>" class="btn btn-sm btn-outline-primary">编辑</a>
|
||||
<a href="manage_versions.php?app_id=<?php echo $app['id']; ?>" class="btn btn-sm btn-outline-secondary">版本管理</a>
|
||||
<a href="deleteapp.php?id=<?php echo $app['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('确定要删除吗?');">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 导航栏滚动效果
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
70
admin/login.php
Normal file
70
admin/login.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
|
||||
// 检查管理员登录状态
|
||||
session_start();
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username']) && isset($_POST['password'])) {
|
||||
$username = $_POST['username'];
|
||||
$password = $_POST['password'];
|
||||
|
||||
if ($username === ADMIN_USERNAME && $password === ADMIN_PASSWORD) {
|
||||
$_SESSION['admin'] = true;
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = '用户名或密码错误';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示登录表单
|
||||
echo '<!DOCTYPE html>';
|
||||
echo '<html lang="zh-CN">';
|
||||
echo '<head>';
|
||||
echo ' <meta charset="UTF-8">';
|
||||
echo ' <meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
||||
echo ' <title>管理员登录 - '. APP_STORE_NAME . '</title>';
|
||||
echo ' <!-- Bootstrap CSS -->';
|
||||
echo ' <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">';
|
||||
echo ' <!-- 自定义CSS -->';
|
||||
echo ' <link rel="stylesheet" href="../styles.css">';
|
||||
echo ' <!-- Fluent Design 模糊效果 -->';
|
||||
echo ' <style>';
|
||||
echo ' .blur-bg {';
|
||||
echo ' backdrop-filter: blur(10px);';
|
||||
echo ' background-color: rgba(255, 255, 255, 0.5);';
|
||||
echo ' }';
|
||||
echo ' </style>';
|
||||
echo '</head>';
|
||||
echo '<body>';
|
||||
echo ' <div class="container mt-5">';
|
||||
echo ' <div class="row justify-content-center">';
|
||||
echo ' <div class="col-md-6">';
|
||||
echo ' <div class="card blur-bg">';
|
||||
echo ' <div class="card-header">管理员登录</div>';
|
||||
echo ' <div class="card-body">';
|
||||
if (isset($error)) {
|
||||
echo ' <div class="alert alert-danger">'. $error . '</div>';
|
||||
}
|
||||
echo ' <form method="post">';
|
||||
echo ' <div class="mb-3">';
|
||||
echo ' <label for="username" class="form-label">用户名</label>';
|
||||
echo ' <input type="text" class="form-control" id="username" name="username" required>';
|
||||
echo ' </div>';
|
||||
echo ' <div class="mb-3">';
|
||||
echo ' <label for="password" class="form-label">密码</label>';
|
||||
echo ' <input type="password" class="form-control" id="password" name="password" required>';
|
||||
echo ' </div>';
|
||||
echo ' <button type="submit" class="btn btn-primary">登录</button>';
|
||||
echo ' </form>';
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
echo ' </div>';
|
||||
echo ' <!-- Bootstrap JS Bundle with Popper -->';
|
||||
echo ' <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>';
|
||||
echo '</body>';
|
||||
echo '</html>';
|
||||
exit;
|
||||
}
|
||||
362
admin/manage_versions.php
Normal file
362
admin/manage_versions.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
|
||||
session_start();
|
||||
// 检查管理员登录状态
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 验证App ID
|
||||
if (!isset($_GET['app_id']) || !is_numeric($_GET['app_id'])) {
|
||||
header('Location: index.php?error=无效的App ID');
|
||||
exit;
|
||||
}
|
||||
$appId = $_GET['app_id'];
|
||||
|
||||
// 获取App信息
|
||||
$app = null;
|
||||
$getAppSql = "SELECT * FROM apps WHERE id = ?";
|
||||
$stmt = $conn->prepare($getAppSql);
|
||||
$stmt->bind_param("i", $appId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($result->num_rows === 0) {
|
||||
header('Location: index.php?error=App不存在');
|
||||
exit;
|
||||
}
|
||||
$app = $result->fetch_assoc();
|
||||
|
||||
// 获取所有版本
|
||||
$versions = [];
|
||||
$getVersionsSql = "SELECT * FROM app_versions WHERE app_id = ? ORDER BY created_at DESC";
|
||||
$stmt = $conn->prepare($getVersionsSql);
|
||||
$stmt->bind_param("i", $appId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$versions[] = $row;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// 处理添加版本
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_version'])) {
|
||||
$version = $_POST['version'];
|
||||
$changelog = $_POST['changelog'];
|
||||
|
||||
if (empty($version)) {
|
||||
$error = '版本号不能为空';
|
||||
} elseif (empty($_FILES['app_file']['name'])) {
|
||||
$error = '请上传App文件';
|
||||
} else {
|
||||
$uploadDir = '../files/';
|
||||
$fileName = basename($_FILES['app_file']['name']);
|
||||
$targetPath = $uploadDir . $fileName;
|
||||
|
||||
if (move_uploaded_file($_FILES['app_file']['tmp_name'], $targetPath)) {
|
||||
$insertVersionSql = "INSERT INTO app_versions (app_id, version, changelog, file_path, created_at) VALUES (?, ?, ?, ?, NOW())";
|
||||
$stmt = $conn->prepare($insertVersionSql);
|
||||
$stmt->bind_param("isss", $appId, $version, $changelog, $targetPath);
|
||||
|
||||
if ($stmt->execute() === TRUE) {
|
||||
header('Location: manage_versions.php?app_id=' . $appId . '&success=版本添加成功');
|
||||
exit;
|
||||
} else {
|
||||
$error = '版本添加失败: ' . $conn->error;
|
||||
unlink($targetPath); // 删除已上传的文件
|
||||
}
|
||||
} else {
|
||||
$error = '文件上传失败';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理删除版本
|
||||
if (isset($_GET['delete_id']) && is_numeric($_GET['delete_id'])) {
|
||||
$versionId = $_GET['delete_id'];
|
||||
|
||||
// 获取版本信息
|
||||
$getVersionSql = "SELECT file_path FROM app_versions WHERE id = ? AND app_id = ?";
|
||||
$stmt = $conn->prepare($getVersionSql);
|
||||
$stmt->bind_param("ii", $versionId, $appId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows === 1) {
|
||||
$version = $result->fetch_assoc();
|
||||
|
||||
// 删除文件
|
||||
if (file_exists($version['file_path'])) {
|
||||
unlink($version['file_path']);
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
$deleteVersionSql = "DELETE FROM app_versions WHERE id = ? AND app_id = ?";
|
||||
$stmt = $conn->prepare($deleteVersionSql);
|
||||
$stmt->bind_param("ii", $versionId, $appId);
|
||||
|
||||
if ($stmt->execute() === TRUE) {
|
||||
header('Location: manage_versions.php?app_id=' . $appId . '&success=版本删除成功');
|
||||
exit;
|
||||
} else {
|
||||
$error = '版本删除失败: ' . $conn->error;
|
||||
}
|
||||
} else {
|
||||
$error = '版本不存在';
|
||||
}
|
||||
}
|
||||
|
||||
// 处理编辑版本
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_version'])) {
|
||||
$versionId = $_POST['version_id'];
|
||||
$version = $_POST['version'];
|
||||
$changelog = $_POST['changelog'];
|
||||
|
||||
if (empty($version)) {
|
||||
$error = '版本号不能为空';
|
||||
} else {
|
||||
// 检查是否上传了新文件
|
||||
$fileUpdate = '';
|
||||
$params = ['ss', $version, $changelog, $versionId, $appId];
|
||||
|
||||
if (!empty($_FILES['new_app_file']['name'])) {
|
||||
$uploadDir = '../files/';
|
||||
$fileName = basename($_FILES['new_app_file']['name']);
|
||||
$targetPath = $uploadDir . $fileName;
|
||||
|
||||
if (move_uploaded_file($_FILES['new_app_file']['tmp_name'], $targetPath)) {
|
||||
// 获取旧文件路径
|
||||
$getOldFileSql = "SELECT file_path FROM app_versions WHERE id = ? AND app_id = ?";
|
||||
$stmt = $conn->prepare($getOldFileSql);
|
||||
$stmt->bind_param("ii", $versionId, $appId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$oldVersion = $result->fetch_assoc();
|
||||
|
||||
// 删除旧文件
|
||||
if (file_exists($oldVersion['file_path'])) {
|
||||
unlink($oldVersion['file_path']);
|
||||
}
|
||||
|
||||
$fileUpdate = ", file_path = ?";
|
||||
$params[0] = 'sss';
|
||||
$params[] = $targetPath;
|
||||
} else {
|
||||
$error = '文件上传失败';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($error)) {
|
||||
$updateVersionSql = "UPDATE app_versions SET version = ?, changelog = ?" . $fileUpdate . " WHERE id = ? AND app_id = ?";
|
||||
$stmt = $conn->prepare($updateVersionSql);
|
||||
|
||||
// 动态绑定参数
|
||||
$stmt->bind_param(...$params);
|
||||
|
||||
if ($stmt->execute() === TRUE) {
|
||||
header('Location: manage_versions.php?app_id=' . $appId . '&success=版本更新成功');
|
||||
exit;
|
||||
} else {
|
||||
$error = '版本更新失败: ' . $conn->error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取URL参数中的成功/错误消息
|
||||
if (isset($_GET['success'])) {
|
||||
$success = $_GET['success'];
|
||||
} elseif (isset($_GET['error'])) {
|
||||
$error = $_GET['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 htmlspecialchars($app['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>
|
||||
.version-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.version-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
.action-btn {
|
||||
margin: 0 2px;
|
||||
}
|
||||
.modal-backdrop {
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<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="editapp.php?id=<?php echo $appId; ?>">返回编辑App</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="manage_versions.php?app_id=<?php echo $appId; ?>">管理版本</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="?logout=true">退出登录</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1>管理版本: <?php echo htmlspecialchars($app['name']); ?></h1>
|
||||
<p class="text-muted">管理该应用的所有版本</p>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addVersionModal">
|
||||
添加新版本
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?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; ?>
|
||||
|
||||
<?php if (empty($versions)): ?>
|
||||
<div class="alert alert-info">
|
||||
暂无版本记录
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row">
|
||||
<?php foreach ($versions as $version): ?>
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card version-card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">版本 <?php echo htmlspecialchars($version['version']); ?></h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">发布日期: <?php echo date('Y-m-d H:i', strtotime($version['created_at'])); ?></h6>
|
||||
<p class="card-text"><?php echo nl2br(htmlspecialchars($version['changelog'])); ?></p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">文件大小: <?php
|
||||
$filePath = $version['file_path'];
|
||||
if (file_exists($filePath)) {
|
||||
echo filesize($filePath) > 0 ? number_format(filesize($filePath) / 1024 / 1024, 2) . ' MB' : '未知';
|
||||
} else {
|
||||
echo '文件不存在';
|
||||
}
|
||||
?></small>
|
||||
<div> <button type="button" class="btn btn-sm btn-outline-secondary action-btn" data-bs-toggle="modal" data-bs-target="#editVersionModal_<?php echo $version['id']; ?>"> 编辑 </button> <a href="../<?php echo htmlspecialchars($version['file_path']); ?>" class="btn btn-sm btn-primary action-btn" download>下载</a> <a href="?app_id=<?php echo $appId; ?>&delete_id=<?php echo $version['id']; ?>" class="btn btn-sm btn-outline-danger action-btn" onclick="return confirm('确定要删除此版本吗?');"> 删除 </a> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑版本模态框 -->
|
||||
<div class="modal fade" id="editVersionModal_<?php echo $version['id']; ?>" tabindex="-1" aria-labelledby="editVersionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editVersionModalLabel">编辑版本 <?php echo htmlspecialchars($version['version']); ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="version_id" value="<?php echo $version['id']; ?>">
|
||||
<div class="mb-3">
|
||||
<label for="version_<?php echo $version['id']; ?>" class="form-label">版本号</label>
|
||||
<input type="text" class="form-control" id="version_<?php echo $version['id']; ?>" name="version" value="<?php echo htmlspecialchars($version['version']); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="changelog_<?php echo $version['id']; ?>" class="form-label">更新日志</label>
|
||||
<textarea class="form-control" id="changelog_<?php echo $version['id']; ?>" name="changelog" rows="3" required><?php echo htmlspecialchars($version['changelog']); ?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_app_file_<?php echo $version['id']; ?>" class="form-label">更新App文件 (可选)</label>
|
||||
<input class="form-control" type="file" id="new_app_file_<?php echo $version['id']; ?>" name="new_app_file">
|
||||
<div class="form-text">当前文件: <?php echo basename($version['file_path']); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-primary" name="edit_version">保存更改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 添加版本模态框 -->
|
||||
<div class="modal fade" id="addVersionModal" tabindex="-1" aria-labelledby="addVersionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addVersionModalLabel">添加新版本</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="version" class="form-label">版本号</label>
|
||||
<input type="text" class="form-control" id="version" name="version" placeholder="如: 1.0.0" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="changelog" class="form-label">更新日志</label>
|
||||
<textarea class="form-control" id="changelog" name="changelog" rows="3" placeholder="描述本次更新内容" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="app_file" class="form-label">App文件</label>
|
||||
<input class="form-control" type="file" id="app_file" name="app_file" required>
|
||||
<a href="<?php echo htmlspecialchars($version['file_path']); ?>" class="btn btn-sm btn-primary" download>下载</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-primary" name="add_version">添加版本</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 导航栏滚动效果
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
388
api.php
Normal file
388
api.php
Normal file
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// 支持查询参数路由模式(不依赖URL重写)
|
||||
if (isset($_GET['action'])) {
|
||||
$action = $_GET['action'];
|
||||
|
||||
// 处理应用列表请求
|
||||
if ($action === 'list' && $requestMethod === 'GET') {
|
||||
$sql = "SELECT apps.id, apps.name, apps.description, apps.age_rating, apps.platform, AVG(reviews.rating) as avg_rating
|
||||
FROM apps
|
||||
LEFT JOIN reviews ON apps.id = reviews.app_id";
|
||||
|
||||
$conditions = [];
|
||||
$stmtParams = [];
|
||||
$paramTypes = '';
|
||||
|
||||
// 搜索功能
|
||||
if (isset($_GET['search'])) {
|
||||
$search = '%' . $_GET['search'] . '%';
|
||||
$conditions[] = "(apps.name LIKE ? OR apps.description LIKE ?)";
|
||||
$stmtParams[] = &$search;
|
||||
$stmtParams[] = &$search;
|
||||
$paramTypes .= 'ss';
|
||||
}
|
||||
|
||||
// 平台过滤
|
||||
if (isset($_GET['platform'])) {
|
||||
$platform = $_GET['platform'];
|
||||
$conditions[] = "apps.platform = ?";
|
||||
$stmtParams[] = &$platform;
|
||||
$paramTypes .= 's';
|
||||
}
|
||||
|
||||
// 年龄分级过滤
|
||||
if (isset($_GET['age_rating'])) {
|
||||
$ageRating = $_GET['age_rating'];
|
||||
$conditions[] = "apps.age_rating = ?";
|
||||
$stmtParams[] = &$ageRating;
|
||||
$paramTypes .= 's';
|
||||
}
|
||||
|
||||
// 分页参数处理
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$limit = isset($_GET['limit']) ? min(100, max(1, intval($_GET['limit']))) : 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$sql .= " WHERE " . implode(" AND ", $conditions);
|
||||
}
|
||||
|
||||
// 添加分页
|
||||
$sql .= " GROUP BY apps.id, apps.name, apps.description, apps.age_rating, apps.platform ORDER BY apps.created_at DESC LIMIT ? OFFSET ?";
|
||||
$stmtParams[] = &$limit;
|
||||
$stmtParams[] = &$offset;
|
||||
$paramTypes .= 'ii';
|
||||
|
||||
// 获取总数用于分页元数据
|
||||
$countSql = "SELECT COUNT(DISTINCT apps.id) as total FROM apps LEFT JOIN reviews ON apps.id = reviews.app_id";
|
||||
if (!empty($conditions)) {
|
||||
$countSql .= " WHERE " . implode(" AND ", $conditions);
|
||||
}
|
||||
$countStmt = $conn->prepare($countSql);
|
||||
if ($paramTypes && count($stmtParams) > 2) {
|
||||
// 排除最后两个分页参数
|
||||
$countParams = array_slice($stmtParams, 0, -2);
|
||||
$countTypes = substr($paramTypes, 0, -2);
|
||||
call_user_func_array([$countStmt, 'bind_param'], array_merge([$countTypes], $countParams));
|
||||
}
|
||||
$countStmt->execute();
|
||||
$countResult = $countStmt->get_result();
|
||||
$total = $countResult->fetch_assoc()['total'] ?? 0;
|
||||
$totalPages = ceil($total / $limit);
|
||||
|
||||
// 执行主查询
|
||||
$stmt = $conn->prepare($sql);
|
||||
call_user_func_array([$stmt, 'bind_param'], array_merge([$paramTypes], $stmtParams));
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$apps = [];
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$apps[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回带分页元数据的响应
|
||||
echo json_encode([
|
||||
'data' => $apps,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'totalPages' => $totalPages
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 处理应用详情请求
|
||||
elseif ($action === 'app' && isset($_GET['id']) && is_numeric($_GET['id']) && $requestMethod === 'GET') {
|
||||
$appId = $_GET['id'];
|
||||
error_log("Requesting app details for ID: $appId");
|
||||
|
||||
$sqlApp = "SELECT apps.id, apps.name, apps.description, apps.age_rating, apps.platform, apps.created_at, AVG(reviews.rating) as avg_rating
|
||||
FROM apps
|
||||
LEFT JOIN reviews ON apps.id = reviews.app_id
|
||||
WHERE apps.id = ?
|
||||
GROUP BY apps.id, apps.name, apps.description, apps.age_rating, apps.platform, apps.created_at";
|
||||
$stmt = $conn->prepare($sqlApp);
|
||||
$stmt->bind_param("i", $appId);
|
||||
$stmt->execute();
|
||||
$resultApp = $stmt->get_result();
|
||||
error_log("Executing prepared statement for app details");
|
||||
|
||||
if (!$resultApp) {
|
||||
error_log("Database error: " . $conn->error);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Database query failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$app = $resultApp->fetch_assoc();
|
||||
error_log("App found: " . ($app ? "Yes" : "No"));
|
||||
|
||||
if ($app) {
|
||||
// 获取版本信息
|
||||
$sqlVersions = "SELECT * FROM app_versions WHERE app_id = $appId ORDER BY created_at DESC";
|
||||
$resultVersions = $conn->query($sqlVersions);
|
||||
$versions = [];
|
||||
while ($version = $resultVersions->fetch_assoc()) {
|
||||
$versions[] = $version;
|
||||
}
|
||||
$app['versions'] = $versions;
|
||||
|
||||
// 获取图片信息
|
||||
$sqlImages = "SELECT * FROM app_images WHERE app_id = $appId";
|
||||
$resultImages = $conn->query($sqlImages);
|
||||
$images = [];
|
||||
while ($image = $resultImages->fetch_assoc()) {
|
||||
$images[] = $image;
|
||||
}
|
||||
$app['images'] = $images;
|
||||
|
||||
// 获取评价信息
|
||||
$sqlReviews = "SELECT * FROM reviews WHERE app_id = $appId ORDER BY created_at DESC";
|
||||
$resultReviews = $conn->query($sqlReviews);
|
||||
$reviews = [];
|
||||
while ($review = $resultReviews->fetch_assoc()) {
|
||||
$reviews[] = $review;
|
||||
}
|
||||
$app['reviews'] = $reviews;
|
||||
|
||||
echo json_encode($app);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => "App with ID $appId not found", 'sql' => $sqlApp]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 处理用户收藏应用
|
||||
elseif ($action === 'favorite' && isset($_GET['app_id']) && is_numeric($_GET['app_id']) && isset($_GET['user_id']) && is_numeric($_GET['user_id']) && $requestMethod === 'POST') {
|
||||
$appId = $_GET['app_id'];
|
||||
$userId = $_GET['user_id'];
|
||||
|
||||
$stmt = $conn->prepare("INSERT IGNORE INTO user_favorites (user_id, app_id) VALUES (?, ?)");
|
||||
$stmt->bind_param("ii", $userId, $appId);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'App added to favorites']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to add to favorites']);
|
||||
}
|
||||
$stmt->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// 获取用户收藏列表
|
||||
elseif ($action === 'favorites' && isset($_GET['user_id']) && is_numeric($_GET['user_id']) && $requestMethod === 'GET') {
|
||||
$userId = $_GET['user_id'];
|
||||
|
||||
$sql = "SELECT apps.* FROM user_favorites JOIN apps ON user_favorites.app_id = apps.id WHERE user_favorites.user_id = $userId";
|
||||
$result = $conn->query($sql);
|
||||
|
||||
$favorites = [];
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$favorites[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($favorites);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 获取应用推荐列表
|
||||
elseif ($action === 'recommendations' && $requestMethod === 'GET') {
|
||||
$sql = "SELECT apps.*, app_recommendations.reason FROM app_recommendations JOIN apps ON app_recommendations.app_id = apps.id";
|
||||
$result = $conn->query($sql);
|
||||
|
||||
$recommendations = [];
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$recommendations[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($recommendations);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 获取热门应用排行榜
|
||||
elseif ($action === 'hot_apps' && $requestMethod === 'GET') {
|
||||
$sql = "SELECT apps.*, SUM(app_versions.download_count) as total_downloads FROM apps JOIN app_versions ON apps.id = app_versions.app_id GROUP BY apps.id ORDER BY total_downloads DESC LIMIT 10";
|
||||
$result = $conn->query($sql);
|
||||
|
||||
$hotApps = [];
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$hotApps[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($hotApps);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 提交用户反馈
|
||||
elseif ($action === 'feedback' && isset($_GET['user_id']) && is_numeric($_GET['user_id']) && $requestMethod === 'POST') {
|
||||
$userId = $_GET['user_id'];
|
||||
$appId = isset($_GET['app_id']) && is_numeric($_GET['app_id']) ? $_GET['app_id'] : null;
|
||||
$content = $_POST['content'] ?? '';
|
||||
|
||||
if (empty($content)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Feedback content is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO user_feedback (user_id, app_id, content) VALUES (?, ?, ?)");
|
||||
$stmt->bind_param("iis", $userId, $appId, $content);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Feedback submitted successfully']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to submit feedback']);
|
||||
}
|
||||
$stmt->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// 处理下载请求
|
||||
elseif ($action === 'download' && isset($_GET['version_id']) && is_numeric($_GET['version_id']) && $requestMethod === 'GET') {
|
||||
$versionId = $_GET['version_id'];
|
||||
|
||||
$stmt = $conn->prepare("SELECT * FROM app_versions WHERE id = ?");
|
||||
$stmt->bind_param("i", $versionId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
$version = $result->fetch_assoc();
|
||||
// 更新下载计数
|
||||
$updateStmt = $conn->prepare("UPDATE app_versions SET download_count = download_count + 1 WHERE id = ?");
|
||||
$updateStmt->bind_param("i", $versionId);
|
||||
$updateStmt->execute();
|
||||
$updateStmt->close();
|
||||
$filePath = $version['file_path'];
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
readfile($filePath);
|
||||
exit;
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'File not found']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Version not found']);
|
||||
}
|
||||
$stmt->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// 无效操作
|
||||
else {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid action or parameters']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 保留原路径路由逻辑作为兼容 fallback
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
$path = parse_url($requestUri, PHP_URL_PATH);
|
||||
$path = preg_replace('/\.php$/', '', $path);
|
||||
$pathParts = explode('/', trim($path, '/'));
|
||||
error_log("Path parts: " . print_r($pathParts, true));
|
||||
|
||||
if ($pathParts[0] === 'api') {
|
||||
error_log("Processing API path request: " . print_r($pathParts, true));
|
||||
// 处理应用详情请求 /api/app/<id>
|
||||
if (count($pathParts) >= 3 && $pathParts[1] === 'app' && is_numeric($pathParts[2])) {
|
||||
$appId = $pathParts[2];
|
||||
error_log("Path-based app details request for ID: $appId");
|
||||
|
||||
$sqlApp = "SELECT apps.id, apps.name, apps.description, apps.age_rating, apps.platform, apps.created_at, AVG(reviews.rating) as avg_rating
|
||||
FROM apps
|
||||
LEFT JOIN reviews ON apps.id = reviews.app_id
|
||||
WHERE apps.id = ?
|
||||
GROUP BY apps.id, apps.name, apps.description, apps.age_rating, apps.platform, apps.created_at";
|
||||
$stmt = $conn->prepare($sqlApp);
|
||||
$stmt->bind_param("i", $appId);
|
||||
$stmt->execute();
|
||||
$resultApp = $stmt->get_result();
|
||||
error_log("Executing prepared statement for path-based app details");
|
||||
|
||||
if (!$resultApp) {
|
||||
error_log("Database error: " . $conn->error);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Database query failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$app = $resultApp->fetch_assoc();
|
||||
error_log("App found via path: " . ($app ? "Yes" : "No"));
|
||||
|
||||
if ($app) {
|
||||
// 获取版本信息
|
||||
$sqlVersions = "SELECT * FROM app_versions WHERE app_id = $appId ORDER BY created_at DESC";
|
||||
$resultVersions = $conn->query($sqlVersions);
|
||||
$versions = [];
|
||||
while ($version = $resultVersions->fetch_assoc()) {
|
||||
$versions[] = $version;
|
||||
}
|
||||
$app['versions'] = $versions;
|
||||
|
||||
// 获取图片信息
|
||||
$sqlImages = "SELECT * FROM app_images WHERE app_id = $appId";
|
||||
$resultImages = $conn->query($sqlImages);
|
||||
$images = [];
|
||||
while ($image = $resultImages->fetch_assoc()) {
|
||||
$images[] = $image;
|
||||
}
|
||||
$app['images'] = $images;
|
||||
|
||||
// 获取评价信息
|
||||
$sqlReviews = "SELECT * FROM reviews WHERE app_id = $appId ORDER BY created_at DESC";
|
||||
$resultReviews = $conn->query($sqlReviews);
|
||||
$reviews = [];
|
||||
while ($review = $resultReviews->fetch_assoc()) {
|
||||
$reviews[] = $review;
|
||||
}
|
||||
$app['reviews'] = $reviews;
|
||||
|
||||
echo json_encode($app);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'error' => 'Not found',
|
||||
'path' => $path,
|
||||
'path_parts' => $pathParts,
|
||||
'action' => isset($_GET['action']) ? $_GET['action'] : null
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
// 处理其他API路径请求...
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'error' => 'Not found',
|
||||
'path' => $path,
|
||||
'path_parts' => $pathParts,
|
||||
'action' => isset($_GET['action']) ? $_GET['action'] : null
|
||||
]);
|
||||
?>
|
||||
193
app.php
Normal file
193
app.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'config.php';
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$appId = $_GET['id'];
|
||||
|
||||
// 获取App信息
|
||||
$sqlApp = "SELECT apps.*, AVG(reviews.rating) as avg_rating
|
||||
FROM apps
|
||||
LEFT JOIN reviews ON apps.id = reviews.app_id
|
||||
WHERE apps.id = $appId
|
||||
GROUP BY apps.id";
|
||||
$resultApp = $conn->query($sqlApp);
|
||||
$app = $resultApp->fetch_assoc();
|
||||
|
||||
if (!$app) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 获取App版本信息
|
||||
$sqlVersions = "SELECT * FROM app_versions WHERE app_id = $appId ORDER BY created_at DESC";
|
||||
$resultVersions = $conn->query($sqlVersions);
|
||||
|
||||
// 获取App预览图片
|
||||
$sqlImages = "SELECT * FROM app_images WHERE app_id = $appId";
|
||||
$resultImages = $conn->query($sqlImages);
|
||||
|
||||
// 获取评价信息
|
||||
$sqlReviews = "SELECT * FROM reviews WHERE app_id = $appId ORDER BY created_at DESC";
|
||||
$resultReviews = $conn->query($sqlReviews);
|
||||
|
||||
// 处理评价提交
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['rating'])) {
|
||||
$rating = $_POST['rating'];
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
$insertSql = "INSERT INTO reviews (app_id, ip_address, rating) VALUES ($appId, '$ipAddress', $rating)";
|
||||
if ($conn->query($insertSql) === TRUE) {
|
||||
header('Location: app.php?id=$appId');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!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['name']; ?> - <?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);
|
||||
}
|
||||
</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">首页</a>
|
||||
</li>
|
||||
<?php if (isset($_SESSION['admin'])): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">管理</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h1><?php echo $app['name']; ?></h1>
|
||||
<p class="lead"><?php echo $app['description']; ?></p>
|
||||
<p>年龄分级: <?php echo $app['age_rating']; ?></p>
|
||||
<?php if (!empty($app['age_rating_description'])): ?>
|
||||
<div class="age-rating-description">
|
||||
<h4>年龄分级说明</h4>
|
||||
<p><?php echo nl2br(htmlspecialchars($app['age_rating_description'])); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<p>适用平台: <?php echo implode(', ', json_decode($app['platforms'], true) ?? []); ?></p>
|
||||
<p>评分: <?php echo round($app['avg_rating'], 1); ?>/5</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div id="imageCarousel" class="carousel slide" data-bs-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
<?php
|
||||
$first = true;
|
||||
while ($image = $resultImages->fetch_assoc()) {
|
||||
$active = $first ? 'active' : '';
|
||||
echo '<div class="carousel-item '. $active . '">';
|
||||
echo '<img src="'. $image['image_path'] . '" class="d-block w-100" alt="App Image">';
|
||||
echo '</div>';
|
||||
$first = false;
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#imageCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#imageCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<h2>版本历史</h2>
|
||||
<?php while ($version = $resultVersions->fetch_assoc()): ?>
|
||||
<div class="card mb-3 blur-bg">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">版本 <?php echo $version['version']; ?></h5>
|
||||
<p class="card-text"><?php echo $version['changelog']; ?></p>
|
||||
<a href="<?php echo htmlspecialchars($version['file_path']); ?>" class="btn btn-primary btn-lg" download>立即下载</a>
|
||||
<a href="version_list.php?id=<?php echo $app['id']; ?>" class="btn btn-outline-secondary">查看版本历史</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endwhile; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<h2>评价</h2>
|
||||
<?php while ($review = $resultReviews->fetch_assoc()): ?>
|
||||
<div class="card mb-3 blur-bg">
|
||||
<div class="card-body">
|
||||
<p class="card-text">评分: <?php echo $review['rating']; ?>/5</p>
|
||||
<p class="card-text"><small class="text-muted">评价时间: <?php echo $review['created_at']; ?></small></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endwhile; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h2>提交评价</h2>
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label for="rating" class="form-label">评分 (1-5星)</label>
|
||||
<select class="form-select" id="rating" name="rating" required>
|
||||
<option value="1">1星</option>
|
||||
<option value="2">2星</option>
|
||||
<option value="3">3星</option>
|
||||
<option value="4">4星</option>
|
||||
<option value="5">5星</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">提交评价</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 导航栏滚动效果
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
191
app_store.sql
Normal file
191
app_store.sql
Normal file
@@ -0,0 +1,191 @@
|
||||
-- 创建数据库
|
||||
-- CREATE DATABASE IF NOT EXISTS app_store;
|
||||
USE awa;
|
||||
|
||||
-- 创建APP表
|
||||
CREATE TABLE IF NOT EXISTS apps (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
age_rating ENUM('3+', '7+', '12+', '17+') NOT NULL,
|
||||
age_rating_description TEXT,
|
||||
platforms JSON NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建APP版本表
|
||||
CREATE TABLE IF NOT EXISTS app_versions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
app_id INT NOT NULL,
|
||||
version VARCHAR(50) NOT NULL,
|
||||
changelog TEXT NOT NULL,
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建APP预览图片表
|
||||
CREATE TABLE IF NOT EXISTS app_images (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
app_id INT NOT NULL,
|
||||
image_path VARCHAR(255) NOT NULL,
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建评价表
|
||||
CREATE TABLE IF NOT EXISTS reviews (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
app_id INT NOT NULL,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
rating TINYINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_review (app_id, ip_address),
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建管理员表
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
-- 插入默认管理员
|
||||
INSERT IGNORE INTO admins (username, password) VALUES ("admin", "your_admin_password_hash");
|
||||
|
||||
-- 创建用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP NULL
|
||||
);
|
||||
|
||||
-- 创建应用分类表
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
-- 应用与分类的多对多关系表
|
||||
CREATE TABLE IF NOT EXISTS app_categories (
|
||||
app_id INT NOT NULL,
|
||||
category_id INT NOT NULL,
|
||||
PRIMARY KEY (app_id, category_id),
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 修改评价表,支持文字评论并关联用户
|
||||
SET @exist_ip_address = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'awa' AND TABLE_NAME = 'reviews' AND COLUMN_NAME = 'ip_address');
|
||||
SET @sql = IF(@exist_ip_address > 0, 'ALTER TABLE reviews DROP COLUMN ip_address', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并删除unique_review索引(如果存在)
|
||||
SET @exist_unique_review = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = 'awa' AND TABLE_NAME = 'reviews' AND INDEX_NAME = 'unique_review');
|
||||
SET @sql = IF(@exist_unique_review > 0, 'ALTER TABLE reviews DROP INDEX unique_review', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并添加user_id列(如果不存在)
|
||||
SET @exist_user_id = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'awa' AND TABLE_NAME = 'reviews' AND COLUMN_NAME = 'user_id');
|
||||
SET @sql = IF(@exist_user_id = 0, 'ALTER TABLE reviews ADD COLUMN user_id INT', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并添加comment列(如果不存在)
|
||||
SET @exist_comment = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'awa' AND TABLE_NAME = 'reviews' AND COLUMN_NAME = 'comment');
|
||||
SET @sql = IF(@exist_comment = 0, 'ALTER TABLE reviews ADD COLUMN comment TEXT', '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 = 'awa' AND TABLE_NAME = 'reviews' AND COLUMN_NAME = 'user_id' AND CONSTRAINT_NAME = 'fk_reviews_users');
|
||||
SET @sql = IF(@exist_fk = 0, 'ALTER TABLE reviews ADD CONSTRAINT fk_reviews_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
-- 检查并添加唯一索引(如果不存在)
|
||||
SET @exist_unique_index = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = 'awa' AND TABLE_NAME = 'reviews' AND INDEX_NAME = 'unique_user_app_review');
|
||||
SET @sql = IF(@exist_unique_index = 0, 'ALTER TABLE reviews ADD UNIQUE KEY unique_user_app_review (user_id, app_id)', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 添加应用下载统计
|
||||
SET @exist_download_count = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'awa' AND TABLE_NAME = 'app_versions' AND COLUMN_NAME = 'download_count');
|
||||
SET @sql = IF(@exist_download_count = 0, 'ALTER TABLE app_versions ADD COLUMN download_count INT DEFAULT 0', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 创建下载历史表
|
||||
CREATE TABLE IF NOT EXISTS download_history (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
version_id INT NOT NULL,
|
||||
download_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (version_id) REFERENCES app_versions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建用户收藏表
|
||||
CREATE TABLE IF NOT EXISTS user_favorites (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
app_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_favorite (user_id, app_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建应用推荐表
|
||||
CREATE TABLE IF NOT EXISTS app_recommendations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
app_id INT NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建应用更新通知表
|
||||
CREATE TABLE IF NOT EXISTS app_update_notifications (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
app_id INT NOT NULL,
|
||||
version_id INT NOT NULL,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (version_id) REFERENCES app_versions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建用户反馈表
|
||||
CREATE TABLE IF NOT EXISTS user_feedback (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
app_id INT,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 修改app_versions表,添加最后更新时间戳用于热门排行
|
||||
SET @exist_last_updated = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'app_versions' AND COLUMN_NAME = 'last_updated');
|
||||
SET @sql = IF(@exist_last_updated = 0, 'ALTER TABLE app_versions ADD COLUMN last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
24
config.php
Normal file
24
config.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
// MySQL 配置
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'awa');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASSWORD', 'ewewew');
|
||||
|
||||
// App Store 名称
|
||||
define('APP_STORE_NAME', 'LeonAPP');
|
||||
|
||||
// 管理员账号
|
||||
define('ADMIN_USERNAME', 'Admin');
|
||||
define('ADMIN_PASSWORD', '123456');
|
||||
|
||||
// 数据库连接
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
die('数据库连接失败: ' . $conn->connect_error);
|
||||
}
|
||||
$conn->set_charset('utf8mb4');
|
||||
|
||||
// 设置时区
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
?>
|
||||
54
download.php
Normal file
54
download.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once 'config.php';
|
||||
|
||||
// 验证版本ID
|
||||
if (!isset($_GET['version_id']) || !is_numeric($_GET['version_id'])) {
|
||||
http_response_code(400);
|
||||
exit('无效的版本ID');
|
||||
}
|
||||
$versionId = $_GET['version_id'];
|
||||
|
||||
// 获取版本信息
|
||||
$version = null;
|
||||
$getVersionSql = "SELECT * FROM app_versions WHERE id = ?";
|
||||
$stmt = $conn->prepare($getVersionSql);
|
||||
$stmt->bind_param("i", $versionId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($result->num_rows !== 1) {
|
||||
http_response_code(404);
|
||||
exit('版本不存在');
|
||||
}
|
||||
$version = $result->fetch_assoc();
|
||||
|
||||
// 获取绝对文件路径
|
||||
$filePath = realpath(__DIR__ . '/' . $version['file_path']);
|
||||
|
||||
// 验证文件存在性
|
||||
if (!$filePath || !file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
exit('文件不存在');
|
||||
}
|
||||
|
||||
// 设置下载响应头
|
||||
$fileName = basename($filePath);
|
||||
$fileSize = filesize($filePath);
|
||||
|
||||
// 清除输出缓冲区并发送 headers
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($fileName));
|
||||
header('Content-Length: ' . $fileSize);
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Expires: 0');
|
||||
header('Pragma: public');
|
||||
|
||||
// 输出文件内容
|
||||
if (!readfile($filePath)) {
|
||||
http_response_code(500);
|
||||
exit('无法读取文件');
|
||||
}
|
||||
|
||||
exit;
|
||||
?>
|
||||
0
files/.gitkeep
Normal file
0
files/.gitkeep
Normal file
BIN
files/LeonChat_1.2.apk
Normal file
BIN
files/LeonChat_1.2.apk
Normal file
Binary file not shown.
4
images/age_12plus.svg
Normal file
4
images/age_12plus.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="#FF9800" />
|
||||
<text x="50" y="55" font-family="Arial" font-size="30" text-anchor="middle" fill="white">12+</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
4
images/age_17plus.svg
Normal file
4
images/age_17plus.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="#F44336" />
|
||||
<text x="50" y="55" font-family="Arial" font-size="30" text-anchor="middle" fill="white">17+</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 226 B |
4
images/age_3plus.svg
Normal file
4
images/age_3plus.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="#4CAF50" />
|
||||
<text x="50" y="55" font-family="Arial" font-size="30" text-anchor="middle" fill="white">3+</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
4
images/age_7plus.svg
Normal file
4
images/age_7plus.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="#FFC107" />
|
||||
<text x="50" y="55" font-family="Arial" font-size="30" text-anchor="middle" fill="white">7+</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
BIN
images/下载.png
Normal file
BIN
images/下载.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
113
index.php
Normal file
113
index.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'config.php';
|
||||
?>
|
||||
<!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);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light blur-bg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#"><?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="#">首页</a>
|
||||
</li>
|
||||
<?php if (isset($_SESSION['admin'])): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">管理</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<form method="get" action="index.php" class="mb-4">
|
||||
<div class="input-group">
|
||||
<input type="text" name="search" class="form-control" placeholder="搜索应用..." value="<?php echo isset($_GET['search']) ? htmlspecialchars($_GET['search']) : ''; ?>">
|
||||
<button class="btn btn-primary" type="submit">搜索</button>
|
||||
</div>
|
||||
</form>
|
||||
<h1>最新应用</h1>
|
||||
<div class="row">
|
||||
<!-- 这里将通过PHP动态加载应用列表 -->
|
||||
<?php
|
||||
$search = isset($_GET['search']) ? $_GET['search'] : '';
|
||||
$sql = "SELECT apps.id, apps.name, apps.description, apps.age_rating, AVG(reviews.rating) as avg_rating
|
||||
FROM apps
|
||||
LEFT JOIN reviews ON apps.id = reviews.app_id ";
|
||||
|
||||
if (!empty($search)) {
|
||||
$sql .= "WHERE apps.name LIKE ? OR apps.description LIKE ? ";
|
||||
}
|
||||
|
||||
$sql .= "GROUP BY apps.id
|
||||
ORDER BY apps.created_at DESC
|
||||
LIMIT 12";
|
||||
|
||||
if (!empty($search)) {
|
||||
$stmt = $conn->prepare($sql);
|
||||
$searchTerm = "%$search%";
|
||||
$stmt->bind_param("ss", $searchTerm, $searchTerm);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
} else {
|
||||
$result = $conn->query($sql);
|
||||
}
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
echo '<div class="col-md-3 mb-4">';
|
||||
echo '<div class="card blur-bg">';
|
||||
echo '<img src="images/default.png" class="card-img-top" alt="'. $row['name'] . '">';
|
||||
echo '<div class="card-body">';
|
||||
echo '<h5 class="card-title">'. $row['name'] . '</h5>';
|
||||
echo '<p class="card-text">'. substr($row['description'], 0, 100) . '...</p>';
|
||||
echo '<p class="card-text">评分: '. round($row['avg_rating'], 1) . '/5</p>';
|
||||
echo '<a href="app.php?id='. $row['id'] . '" class="btn btn-primary">查看详情</a>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 导航栏滚动效果
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
router.php
Normal file
10
router.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// 路由脚本,用于PHP内置服务器
|
||||
// 将所有/api开头的请求转发到api.php
|
||||
if (preg_match('/^\/api/', $_SERVER['REQUEST_URI'])) {
|
||||
include 'api.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// 其他请求按正常方式处理
|
||||
return false;
|
||||
131
styles.css
Normal file
131
styles.css
Normal file
@@ -0,0 +1,131 @@
|
||||
:root {
|
||||
--primary-color: #0078d4;
|
||||
--secondary-color: #f3f2f1;
|
||||
--text-color: #333333;
|
||||
--border-radius: 4px;
|
||||
--card-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
--card-shadow-hover: 0 12px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.age-rating-description {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #0d6efd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.age-rating-description h4 {
|
||||
margin-top: 0;
|
||||
color: #0d6efd;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
color: var(--text-color);
|
||||
background-color: #faf9f8;
|
||||
overflow-x: hidden;
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
/* AppStore风格入场动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.blur-bg {
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
box-shadow: var(--card-shadow);
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 卡片交错动画延迟 */
|
||||
.card:nth-child(2n) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.card:nth-child(3n) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #005a9e;
|
||||
border-color: #005a9e;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(1px) scale(0.98);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.75rem 1rem;
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.navbar.scrolled {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--text-color);
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* 年龄分级 SVG 样式 */
|
||||
.age-rating {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.age-rating svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
128
version_list.php
Normal file
128
version_list.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
// 验证App ID
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: index.php?error=无效的App ID');
|
||||
exit;
|
||||
}
|
||||
$appId = $_GET['id'];
|
||||
|
||||
// 获取App信息
|
||||
$app = null;
|
||||
$getAppSql = "SELECT * FROM apps WHERE id = ?";
|
||||
$stmt = $conn->prepare($getAppSql);
|
||||
$stmt->bind_param("i", $appId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($result->num_rows === 0) {
|
||||
header('Location: index.php?error=App不存在');
|
||||
exit;
|
||||
}
|
||||
$app = $result->fetch_assoc();
|
||||
|
||||
// 获取所有版本
|
||||
$versions = [];
|
||||
$getVersionsSql = "SELECT * FROM app_versions WHERE app_id = ? ORDER BY created_at DESC";
|
||||
$stmt = $conn->prepare($getVersionsSql);
|
||||
$stmt->bind_param("i", $appId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$versions[] = $row;
|
||||
}
|
||||
?>
|
||||
<!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 htmlspecialchars($app['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>
|
||||
.version-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.version-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
.download-btn {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
.download-btn:hover {
|
||||
background-color: #0b5ed7;
|
||||
border-color: #0a58ca;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<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">首页</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="app.php?id=<?php echo $appId; ?>">返回App详情</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1><?php echo htmlspecialchars($app['name']); ?> - 版本历史</h1>
|
||||
<p class="text-muted">查看和下载该应用的所有历史版本</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($versions)): ?>
|
||||
<div class="alert alert-info">
|
||||
暂无版本记录
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row">
|
||||
<?php foreach ($versions as $version): ?>
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card version-card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">版本 <?php echo htmlspecialchars($version['version']); ?></h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">发布日期: <?php echo date('Y-m-d', strtotime($version['created_at'])); ?></h6>
|
||||
<p class="card-text"><?php echo nl2br(htmlspecialchars($version['changelog'])); ?></p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center"> <a href="<?php echo htmlspecialchars($version['file_path']); ?>" class="btn btn-primary" download>下载</a> <small class="text-muted">文件大小: <?php echo $fileSize; ?></small> </div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 导航栏滚动效果
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 10) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
108
windows/console_app.py
Normal file
108
windows/console_app.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
def fetch_apps(search_term=None):
|
||||
url = 'http://localhost:3232/api.php?action=list'
|
||||
if search_term:
|
||||
url += f'&search={search_term}'
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"获取应用列表失败: {e}")
|
||||
return []
|
||||
|
||||
def get_app_details(app_id):
|
||||
url = f'http://localhost:3232/api.php?action=app&id={app_id}'
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if response.status_code == 404:
|
||||
try:
|
||||
error_data = response.json()
|
||||
print(f"获取应用详情失败: {error_data.get('error')}")
|
||||
print(f"执行的SQL: {error_data.get('sql')}")
|
||||
except ValueError:
|
||||
print(f"获取应用详情失败: {e}")
|
||||
else:
|
||||
print(f"获取应用详情失败: {e}")
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"获取应用详情失败: {e}")
|
||||
return None
|
||||
|
||||
def download_app(version_id):
|
||||
url = f'http://localhost:3232/api.php?action=download&version_id={version_id}'
|
||||
try:
|
||||
response = requests.get(f'http://localhost:3232/api/download/{version_id}', stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
filename = response.headers.get('Content-Disposition', '').split('filename=')[-1].strip('"')
|
||||
if not filename:
|
||||
filename = f'app_version_{version_id}.apk'
|
||||
|
||||
with open(filename, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
print(f'下载成功: {filename}')
|
||||
return True
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f'下载失败: {e}')
|
||||
return False
|
||||
|
||||
def display_apps(apps):
|
||||
if not apps:
|
||||
print('没有找到应用程序')
|
||||
return
|
||||
|
||||
print('=== 应用商店 ===')
|
||||
for i, app in enumerate(apps, 1):
|
||||
print(f'[{i}] {app.get("name")}')
|
||||
print(f' 描述: {app.get("description", "无")}')
|
||||
print(f' 评分: {app.get("avg_rating", "暂无")}/5')
|
||||
print(f' 适用平台: {", ".join(app.get("platforms", []))}')
|
||||
|
||||
if __name__ == "__main__":
|
||||
while True:
|
||||
print("\n=== 应用商店控制台 ===")
|
||||
print("1. 浏览所有应用")
|
||||
print("2. 搜索应用")
|
||||
print("3. 查看应用详情")
|
||||
print("4. 下载应用")
|
||||
print("5. 退出")
|
||||
|
||||
choice = input("请选择操作 (1-5): ")
|
||||
|
||||
if choice == "1":
|
||||
apps = fetch_apps()
|
||||
display_apps(apps)
|
||||
elif choice == "2":
|
||||
search_term = input("请输入搜索关键词: ")
|
||||
apps = fetch_apps(search_term)
|
||||
display_apps(apps)
|
||||
elif choice == "3":
|
||||
app_id = input("请输入应用ID: ")
|
||||
app = get_app_details(app_id)
|
||||
if app:
|
||||
print("\n=== 应用详情 ===")
|
||||
print(f"名称: {app.get('name')}")
|
||||
print(f"描述: {app.get('description', '无')}")
|
||||
print(f"评分: {app.get('avg_rating', '暂无')}/5")
|
||||
print(f"适用平台: {', '.join(app.get('platforms', []))}")
|
||||
print("版本:")
|
||||
for version in app.get('versions', []):
|
||||
print(f" - {version.get('version_name')} (ID: {version.get('id')})")
|
||||
else:
|
||||
print("应用不存在或获取失败")
|
||||
elif choice == "4":
|
||||
version_id = input("请输入版本ID: ")
|
||||
download_app(version_id)
|
||||
elif choice == "5":
|
||||
print("谢谢使用,再见!")
|
||||
break
|
||||
else:
|
||||
print("无效的选择,请重试")
|
||||
209
windows/gui_app.py
Normal file
209
windows/gui_app.py
Normal file
@@ -0,0 +1,209 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
import requests
|
||||
import json
|
||||
import threading
|
||||
|
||||
class AppStoreGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("应用商店")
|
||||
self.root.geometry("800x600")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
# 创建搜索框架
|
||||
self.search_frame = ttk.Frame(root)
|
||||
self.search_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
ttk.Label(self.search_frame, text="搜索应用: ").pack(side=tk.LEFT, padx=5)
|
||||
self.search_entry = ttk.Entry(self.search_frame, width=50)
|
||||
self.search_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(self.search_frame, text="搜索", command=self.on_search).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 创建主框架
|
||||
self.main_frame = ttk.Frame(root)
|
||||
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
|
||||
# 创建滚动条
|
||||
self.canvas = tk.Canvas(self.main_frame)
|
||||
self.scrollbar = ttk.Scrollbar(self.main_frame, orient="vertical", command=self.canvas.yview)
|
||||
self.scrollable_frame = ttk.Frame(self.canvas)
|
||||
|
||||
self.scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: self.canvas.configure(
|
||||
scrollregion=self.canvas.bbox("all")
|
||||
)
|
||||
)
|
||||
|
||||
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
||||
self.canvas.configure(yscrollcommand=self.scrollbar.set)
|
||||
|
||||
self.canvas.pack(side="left", fill="both", expand=True)
|
||||
self.scrollbar.pack(side="right", fill="y")
|
||||
|
||||
# 加载应用列表
|
||||
self.load_apps()
|
||||
|
||||
def fetch_apps(self, search_term=None):
|
||||
url = 'http://localhost:3232/api.php?action=list'
|
||||
if search_term:
|
||||
url += f'&search={search_term}'
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
messagebox.showerror('错误', f'获取应用列表失败: {str(e)}')
|
||||
return []
|
||||
|
||||
def fetch_app_details(self, app_id):
|
||||
url = f'http://localhost:3232/api.php?action=app&id={app_id}'
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
messagebox.showerror('错误', f'获取应用详情失败: {str(e)}')
|
||||
return None
|
||||
|
||||
def download_version(self, version_id):
|
||||
url = f'http://localhost:3232/api.php?action=download&version_id={version_id}'
|
||||
try:
|
||||
response = requests.get(f'http://localhost:3232/api/app/{app_id}')
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
messagebox.showerror("错误", f"获取应用详情失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def on_search(self):
|
||||
# 清空现有应用卡片
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
# 加载搜索结果
|
||||
search_term = self.search_entry.get().strip()
|
||||
self.load_apps(search_term)
|
||||
|
||||
def load_apps(self, search_term=None):
|
||||
apps = self.fetch_apps(search_term)
|
||||
if not apps:
|
||||
ttk.Label(self.scrollable_frame, text="没有找到应用程序").pack(pady=20)
|
||||
return
|
||||
|
||||
# 创建应用卡片网格
|
||||
for i, app in enumerate(apps):
|
||||
frame = ttk.LabelFrame(self.scrollable_frame, text=app.get("name"))
|
||||
frame.grid(row=i//2, column=i%2, padx=10, pady=10, sticky="nsew")
|
||||
|
||||
ttk.Label(frame, text=f"描述: {app.get('description', '无')}").pack(anchor="w", padx=5, pady=2)
|
||||
ttk.Label(frame, text=f"评分: {app.get('avg_rating', '暂无')}/5").pack(anchor="w", padx=5, pady=2)
|
||||
ttk.Label(frame, text=f"适用平台: {','.join(app.get('platforms', []))}").pack(anchor="w", padx=5, pady=2)
|
||||
ttk.Button(frame, text="查看详情", command=lambda a=app: self.show_details(a)).pack(pady=5)
|
||||
|
||||
# 配置网格权重使卡片自适应
|
||||
self.scrollable_frame.grid_columnconfigure(0, weight=1)
|
||||
self.scrollable_frame.grid_columnconfigure(1, weight=1)
|
||||
|
||||
def show_details(self, app):
|
||||
# 获取完整应用详情
|
||||
app_details = self.fetch_app_details(app['id'])
|
||||
if not app_details:
|
||||
return
|
||||
|
||||
detail_window = tk.Toplevel(self.root)
|
||||
detail_window.title(app_details.get("name"))
|
||||
detail_window.geometry("600x400")
|
||||
|
||||
# 创建滚动区域
|
||||
canvas = tk.Canvas(detail_window)
|
||||
scrollbar = ttk.Scrollbar(detail_window, orient="vertical", command=canvas.yview)
|
||||
scrollable_frame = ttk.Frame(canvas)
|
||||
|
||||
scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: canvas.configure(
|
||||
scrollregion=canvas.bbox("all")
|
||||
)
|
||||
)
|
||||
|
||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
|
||||
# 应用基本信息
|
||||
ttk.Label(scrollable_frame, text=f"名称: {app_details.get('name')}", font=('Arial', 12, 'bold')).pack(anchor="w", padx=10, pady=5)
|
||||
ttk.Label(scrollable_frame, text=f"描述: {app_details.get('description', '无')}").pack(anchor="w", padx=10, pady=5)
|
||||
ttk.Label(scrollable_frame, text=f"评分: {app_details.get('avg_rating', '暂无')}/5").pack(anchor="w", padx=10, pady=5)
|
||||
ttk.Label(scrollable_frame, text=f"适用年龄: {app_details.get('age_rating', '未知')}").pack(anchor="w", padx=10, pady=5)
|
||||
ttk.Label(scrollable_frame, text=f"适用平台: {','.join(app_details.get('platforms', []))}").pack(anchor="w", padx=10, pady=5)
|
||||
|
||||
# 版本信息
|
||||
versions = app_details.get('versions', [])
|
||||
if versions:
|
||||
ttk.Label(scrollable_frame, text="\n=== 版本列表 ===", font=('Arial', 10, 'bold')).pack(anchor="w", padx=10, pady=10)
|
||||
for version in versions:
|
||||
version_frame = ttk.Frame(scrollable_frame)
|
||||
version_frame.pack(anchor="w", padx=15, pady=5, fill=tk.X)
|
||||
|
||||
ttk.Label(version_frame, text=f"版本 {version.get('version_name', '未知')}", font=('Arial', 9, 'bold')).pack(anchor="w")
|
||||
ttk.Label(version_frame, text=f"发布日期: {version.get('created_at', '未知')}").pack(anchor="w")
|
||||
ttk.Label(version_frame, text=f"文件大小: {version.get('file_size', '未知')}").pack(anchor="w")
|
||||
ttk.Button(version_frame, text="下载", command=lambda v=version: self.download_version(v)).pack(anchor="e", pady=5)
|
||||
|
||||
ttk.Button(scrollable_frame, text="关闭", command=detail_window.destroy).pack(pady=20)
|
||||
|
||||
def download_version(self, version):
|
||||
def download_thread():
|
||||
try:
|
||||
# 询问保存路径
|
||||
save_path = filedialog.asksaveasfilename(
|
||||
defaultextension=".apk",
|
||||
filetypes=[("APK files", "*.apk"), ("All files", "*")],
|
||||
initialfile=version.get('file_name', f"app_{version['id']}.apk")
|
||||
)
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
# 下载文件
|
||||
response = requests.get(f'http://localhost:3232/api/download/{version["id"]}', stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
block_size = 1024 # 1 KB
|
||||
progress = 0
|
||||
|
||||
with open(save_path, 'wb') as file:
|
||||
for data in response.iter_content(block_size):
|
||||
progress += len(data)
|
||||
file.write(data)
|
||||
# 更新进度条
|
||||
progress_var.set((progress / total_size) * 100)
|
||||
root.update_idletasks()
|
||||
|
||||
messagebox.showinfo("成功", f"文件已下载至:\n{save_path}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
messagebox.showerror("错误", f"下载失败: {str(e)}")
|
||||
finally:
|
||||
progress_window.destroy()
|
||||
|
||||
# 创建进度窗口
|
||||
progress_window = tk.Toplevel(self.root)
|
||||
progress_window.title(f"下载版本 {version.get('version_name', '未知')}")
|
||||
progress_window.geometry("300x100")
|
||||
progress_window.resizable(False, False)
|
||||
|
||||
ttk.Label(progress_window, text="下载中...").pack(pady=10)
|
||||
progress_var = tk.DoubleVar()
|
||||
progress_bar = ttk.Progressbar(progress_window, variable=progress_var, maximum=100)
|
||||
progress_bar.pack(fill=tk.X, padx=20, pady=10)
|
||||
|
||||
# 启动下载线程
|
||||
threading.Thread(target=download_thread).start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = AppStoreGUI(root)
|
||||
root.mainloop()
|
||||
2
windows/requirements.txt
Normal file
2
windows/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests==2.31.0
|
||||
pyinstaller==6.8.0
|
||||
Reference in New Issue
Block a user