AbyssalSwamp  ActivaUser
» Guest:  Register | Login | 会员列表

RSS subscription to this AbyssalSwamp  

Previous thread Next thread
     
Title: 我把整套“上传到 SMB + VIP 下载”的完整程序都放在右侧画布里了(包含表结构、配置、上传下载列表页、工具函数)。按你的环境改下 `config.php` 里的数据库和 SMB 账号就能跑。需要我再加“单文件购买接口”“预览图转码”“后台批量导入”等功能,直接说!  
 
xander
超级版主
Rank: 12Rank: 12Rank: 12


UID 1
Digest 2
Points 2
Posts 169
码币MB 309 Code
黄金 0 Catty
钻石 903 Pellet
Permissions 200
Register 2022-2-7
Status offline
我把整套“上传到 SMB + VIP 下载”的完整程序都放在右侧画布里了(包含表结构、配置、上传下载列表页、工具函数)。按你的环境改下 `config.php` 里的数据库和 SMB 账号就能跑。需要我再加“单文件购买接口”“预览图转码”“后台批量导入”等功能,直接说!

# CA381:PHP7.4 基于 SMB 的上传与 VIP 下载(完整示例)


> 说明:本示例以 **php-smbclient** 扩展 + **PDO(MySQL/MariaDB)** 为基础;目录规则按“按用户分目录 + 年/月分层”,文件名为 `{file_id}.{ext}`;下载端实现 **VIP/单品授权** 校验与 **Range** 断点(支持视频拖动)。

>

> 你可以把下面的文件逐个保存到你的 Web 根目录(如 `/home/ca381/www/culture/ca381_media/`)。


---


## 0) 数据表(DDL)——`create_tables.sql`


```sql

-- 会员信息(VIP)

CREATE TABLE IF NOT EXISTS user_memberships (

  user_id        BIGINT UNSIGNED NOT NULL PRIMARY KEY,

  vip_start_at   DATETIME        NULL,

  vip_end_at     DATETIME        NULL,

  created_at     DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,

  updated_at     DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  CONSTRAINT chk_vip_period CHECK (vip_end_at IS NULL OR vip_end_at >= vip_start_at)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 资源表

CREATE TABLE IF NOT EXISTS media_files (

  id               BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,

  user_id          BIGINT UNSIGNED NOT NULL,

  kind             ENUM('doc','image','video','audio','other') NOT NULL,

  mime_type        VARCHAR(127)    NOT NULL,

  ext              VARCHAR(16)     NOT NULL,

  size_bytes       BIGINT UNSIGNED NOT NULL,

  duration_seconds INT UNSIGNED    NULL,

  width_px         INT UNSIGNED    NULL,

  height_px        INT UNSIGNED    NULL,

  sha256           CHAR(64)        NOT NULL,

  original_name    VARCHAR(255)    NOT NULL,

  title            VARCHAR(255)    NULL,

  description      TEXT            NULL,

  storage_uri      VARCHAR(512)    NOT NULL,

  storage_diskpath VARCHAR(512)    NULL,

  vip_required     TINYINT(1)      NOT NULL DEFAULT 1,

  is_public        TINYINT(1)      NOT NULL DEFAULT 0,

  uploaded_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,

  deleted_at       DATETIME        NULL,

  INDEX idx_user              (user_id),

  INDEX idx_kind_time         (kind, uploaded_at),

  INDEX idx_uploaded_at       (uploaded_at),

  UNIQUE KEY uk_user_sha256   (user_id, sha256)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 资源访问授权(单品购买/赠送)

CREATE TABLE IF NOT EXISTS file_access_grants (

  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,

  file_id     BIGINT UNSIGNED NOT NULL,

  user_id     BIGINT UNSIGNED NOT NULL,

  grant_type  ENUM('vip','purchase','admin') NOT NULL,

  expires_at  DATETIME NULL,

  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

  UNIQUE KEY uk_file_user (file_id, user_id),

  INDEX idx_user (user_id),

  INDEX idx_file (file_id)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 下载日志

CREATE TABLE IF NOT EXISTS download_logs (

  id             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,

  file_id        BIGINT UNSIGNED NOT NULL,

  user_id        BIGINT UNSIGNED NOT NULL,

  ip             VARCHAR(45)     NOT NULL,

  user_agent     VARCHAR(255)    NULL,

  bytes_served   BIGINT UNSIGNED NOT NULL,

  downloaded_at  DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_file_time (file_id, downloaded_at),

  INDEX idx_user_time (user_id, downloaded_at)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

```


---


## 1) 配置文件——`config.php`


```php

<?php

// 数据库配置

return [

    'db' => [

        'dsn'      => 'mysql:host=127.0.0.1;port=3306;dbname=ca381;charset=utf8mb4',

        'user'     => 'ca381',

        'password' => 'your_db_password',

        'options'  => [

            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,

            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

            PDO::ATTR_EMULATE_PREPARES   => false,

        ],

    ],


    // SMB 存储配置

    'smb' => [

        // 共享根:\tx01ca381uploads

        // 使用 smb 流访问时拼为 smb://user:pass@tx01/ca381/uploads

        'host'     => 'tx01',

        'share'    => 'ca381',

        'subdir'   => 'uploads',

        'user'     => 'smb_user',

        'password' => 'smb_pass',

        // 若同时挂载到本地以便备份/运维,可填写本地挂载点,留空则不使用

        'local_mount' => '/mnt/smb_ca381/uploads',

    ],


    // 上传策略

    'upload' => [

        'max_bytes' => 1024 * 1024 * 1024, // 1GB 示例

        // 扩展白名单(小写)

        'allowed_exts' => [

            'pdf','doc','docx','xls','xlsx','ppt','pptx','txt','md',

            'jpg','jpeg','png','gif','webp','bmp','svg',

            'mp4','mov','m4v','avi','mkv','webm',

            'mp3','aac','wav','flac','m4a'

        ],

        // 可执行文件黑名单(强制拒绝)

        'blocked_exts' => ['php','phtml','phar','cgi','exe','dll','sh','bat','cmd'],

    ],

];

```


---


## 2) PDO 连接——`db.php`


```php

<?php

$config = require __DIR__ . '/config.php';

$pdo = new PDO(

    $config['db']['dsn'],

    $config['db']['user'],

    $config['db']['password'],

    $config['db']['options']

);

```


---


## 3) 工具函数——`utils.php`


```php

<?php

function current_user_id(): int {

    // 你已有登录系统可从 SESSION 里取;此处示例用会话

    session_start();

    if (!empty($_SESSION['user_id'])) return (int)$_SESSION['user_id'];

    // 开发期可硬编码测试 UID=123

    // $_SESSION['user_id'] = 123; return 123;

    http_response_code(401);

    exit('请先登录');

}


function cfg(): array { return require __DIR__ . '/config.php'; }


function build_smb_base(): string {

    $c = cfg()['smb'];

    $user = rawurlencode($c['user']);

    $pass = rawurlencode($c['password']);

    return sprintf('smb://%s:%s@%s/%s/%s', $user, $pass, $c['host'], $c['share'], trim($c['subdir'], '/'));

}


function norm_ext(string $filename): string {

    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    return preg_replace('/[^a-z0-9]+/','', $ext);

}


function guess_kind(string $mime, string $ext): string {

    if (preg_match('#^image/#', $mime) || in_array($ext, ['jpg','jpeg','png','gif','webp','bmp','svg'])) return 'image';

    if (preg_match('#^video/#', $mime) || in_array($ext, ['mp4','mov','m4v','avi','mkv','webm'])) return 'video';

    if (preg_match('#^audio/#', $mime) || in_array($ext, ['mp3','aac','wav','flac','m4a'])) return 'audio';

    if (in_array($ext, ['pdf','doc','docx','xls','xlsx','ppt','pptx','txt','md'])) return 'doc';

    return 'other';

}


function ensure_allowed_ext(string $ext): void {

    $up = cfg()['upload'];

    if (in_array($ext, $up['blocked_exts'], true)) {

        http_response_code(400); exit('非法文件类型');

    }

    if (!in_array($ext, $up['allowed_exts'], true)) {

        http_response_code(400); exit('不支持的扩展名');

    }

}


function smb_mkdir_recursive(string $uri): bool {

    // 逐层创建(smb:// 不总是支持 recursive=true 的 mkdir)

    $parts = explode('/', trim($uri, '/'));

    $path = '';

    foreach ($parts as $i => $p) {

        $path .= '/' . $p;

        $full = 'smb://' . ltrim($path, '/');

        if ($i < 3) continue; // 跳过 smb://host/share 根层

        if (@is_dir($full)) continue;

        if (!@mkdir($full)) return false;

    }

    return true;

}


function insert_media_placeholder(PDO $pdo, array $row): int {

    $sql = "INSERT INTO media_files

            (user_id, kind, mime_type, ext, size_bytes, sha256, original_name, title, description,

             storage_uri, storage_diskpath, vip_required, is_public)

            VALUES

            (:user_id,:kind,:mime_type,:ext,:size_bytes,:sha256,riginal_name,:title,:description,

             :storage_uri,:storage_diskpath,:vip_required,:is_public)";

    $stmt = $pdo->prepare($sql);

    $stmt->execute([

        ':user_id' => $row['user_id'],

        ':kind' => $row['kind'],

        ':mime_type' => $row['mime_type'],

        ':ext' => $row['ext'],

        ':size_bytes' => $row['size_bytes'],

        ':sha256' => $row['sha256'],

        'riginal_name' => $row['original_name'],

        ':title' => $row['title'] ?? null,

        ':description' => $row['description'] ?? null,

        ':storage_uri' => $row['storage_uri'],

        ':storage_diskpath' => $row['storage_diskpath'] ?? null,

        ':vip_required' => $row['vip_required'] ?? 1,

        ':is_public' => $row['is_public'] ?? 0,

    ]);

    return (int)$pdo->lastInsertId();

}


function update_media_storage(PDO $pdo, int $id, string $uri, ?string $diskpath): void {

    $stmt = $pdo->prepare("UPDATE media_files SET storage_uri=:u, storage_diskpath=:d WHERE id=:id");

    $stmt->execute([':u'=>$uri, ':d'=>$diskpath, ':id'=>$id]);

}


function is_vip(PDO $pdo, int $userId): bool {

    $stmt = $pdo->prepare("SELECT vip_end_at FROM user_memberships WHERE user_id=?");

    $stmt->execute([$userId]);

    $r = $stmt->fetch();

    return $r && (!empty($r['vip_end_at'])) && (strtotime($r['vip_end_at']) >= time());

}


function has_grant(PDO $pdo, int $userId, int $fileId): bool {

    $stmt = $pdo->prepare("SELECT 1 FROM file_access_grants WHERE user_id=? AND file_id=? AND (expires_at IS NULL OR expires_at>=NOW()) LIMIT 1");

    $stmt->execute([$userId, $fileId]);

    return (bool)$stmt->fetchColumn();

}


function can_download(PDO $pdo, int $userId, array $file): bool {

    if (!empty($file['deleted_at'])) return false;

    if ((int)$file['vip_required'] === 0) return true;

    if (is_vip($pdo, $userId)) return true;

    if (has_grant($pdo, $userId, (int)$file['id'])) return true;

    return false;

}


function log_download(PDO $pdo, int $fileId, int $userId, int $bytes): void {

    $stmt = $pdo->prepare("INSERT INTO download_logs (file_id, user_id, ip, user_agent, bytes_served) VALUES (?,?,?,?,?)");

    $ip = $_SERVER['REMOTE_ADDR'] ?? '';

    $ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255);

    $stmt->execute([$fileId, $userId, $ip, $ua, $bytes]);

}

```


---


## 4) 上传表单(示例)——`upload_form.php`


```php

<?php require __DIR__ . '/utils.php'; current_user_id(); ?>

<!doctype html>

<html><head><meta charset="utf-8"><title>上传文件</title></head>

<body>

  <h1>上传到 SMB(按用户/年月归档)</h1>

  <form action="upload_handle.php" method="post" enctype="multipart/form-data">

    <label>标题:<input type="text" name="title" maxlength="255"></label><br>

    <label>描述:<textarea name="description" rows="3" cols="50"></textarea></label><br>

    <label>是否需要VIP:

      <select name="vip_required">

        <option value="1" selected>是(默认)</option>

        <option value="0">否(公开下载或仅预览)</option>

      </select>

    </label><br>

    <label>文件:<input type="file" name="file" required></label><br>

    <button type="submit">上传</button>

  </form>

</body></html>

```


---


## 5) 上传处理(写 SMB + 入库)——`upload_handle.php`


```php

<?php

require __DIR__ . '/db.php';

require __DIR__ . '/utils.php';

$userId = current_user_id();

$cfg = cfg();


if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {

    http_response_code(400); exit('上传失败');

}


$origName = $_FILES['file']['name'];

$tmp = $_FILES['file']['tmp_name'];

$size = (int)$_FILES['file']['size'];

if ($size <= 0 || $size > $cfg['upload']['max_bytes']) {

    http_response_code(400); exit('文件大小不合法');

}


$ext = norm_ext($origName);

ensure_allowed_ext($ext);


$mime = mime_content_type($tmp) ?: 'application/octet-stream';

$kind = guess_kind($mime, $ext);

$sha256 = hash_file('sha256', $tmp);


// 去重:同一用户+内容唯一

$check = $pdo->prepare("SELECT id FROM media_files WHERE user_id=? AND sha256=? AND deleted_at IS NULL LIMIT 1");

$check->execute([$userId, $sha256]);

if ($exist = $check->fetchColumn()) {

    exit('该文件已存在(同一内容已上传,ID='.$exist.')');

}


$title = trim($_POST['title'] ?? '');

$desc  = trim($_POST['description'] ?? '');

$vipRequired = (int)($_POST['vip_required'] ?? 1);


// 先占位一条记录(临时使用占位路径,插入后拿到 file_id)

$placeholder_uri = 'smb://placeholder';

$diskpath = null;

$fileId = insert_media_placeholder($pdo, [

    'user_id' => $userId,

    'kind' => $kind,

    'mime_type' => $mime,

    'ext' => $ext,

    'size_bytes' => $size,

    'sha256' => $sha256,

    'original_name' => $origName,

    'title' => $title ?: null,

    'description' => $desc ?: null,

    'storage_uri' => $placeholder_uri,

    'storage_diskpath' => $diskpath,

    'vip_required' => $vipRequired,

    'is_public' => 0,

]);


$base = build_smb_base(); // smb://user:pass@tx01/ca381/uploads

$year = date('Y'); $month = date('m');

$dir = sprintf('%s/user_%d/%s/%s', $base, $userId, $year, $month);


if (!smb_mkdir_recursive($dir)) { http_response_code(500); exit('创建 SMB 目录失败'); }


$target = $dir . '/' . $fileId . '.' . $ext;

$in  = fopen($tmp, 'rb');

$out = fopen($target, 'wb');

if (!$in || !$out) { http_response_code(500); exit('打开文件句柄失败'); }


$bytes = stream_copy_to_stream($in, $out);

fclose($in); fclose($out);

if ($bytes !== $size) { http_response_code(500); exit('写入不完整'); }


// 更新存储路径

update_media_storage($pdo, $fileId, $target, null);


echo '上传成功,文件ID:' . $fileId;

```


---


## 6) 下载接口(VIP/授权校验 + Range 支持)——`download.php`


```php

<?php

require __DIR__ . '/db.php';

require __DIR__ . '/utils.php';

$userId = current_user_id();


$fileId = (int)($_GET['id'] ?? 0);

if ($fileId <= 0) { http_response_code(400); exit('参数错误'); }


$stmt = $pdo->prepare("SELECT * FROM media_files WHERE id=? LIMIT 1");

$stmt->execute([$fileId]);

$file = $stmt->fetch();

if (!$file) { http_response_code(404); exit('文件不存在'); }


if (!can_download($pdo, $userId, $file)) {

    http_response_code(402); // Payment Required(语义合适)

    exit('需要 VIP 或购买授权');

}


$uri = $file['storage_uri'];

$fp = @fopen($uri, 'rb');

if (!$fp) { http_response_code(500); exit('无法打开存储文件'); }


$size = (int)$file['size_bytes'];

$filename = $file['original_name'];

$mime = $file['mime_type'] ?: 'application/octet-stream';


// Range 处理(支持视频拖动)

$start = 0; $end = $size - 1; $httpStatus = 200;

if (isset($_SERVER['HTTP_RANGE'])) {

    if (preg_match('/bytes=(d+)-(d*)/', $_SERVER['HTTP_RANGE'], $m)) {

        $start = (int)$m[1];

        if ($m[2] !== '') $end = (int)$m[2];

        if ($end >= $size) $end = $size - 1;

        if ($start > $end || $start >= $size) { http_response_code(416); exit; }

        $httpStatus = 206;

    }

}


$length = $end - $start + 1;


// 头部

header_remove('X-Powered-By');

http_response_code($httpStatus);

header("Content-Type: $mime");

header('Accept-Ranges: bytes');

header('Content-Disposition: attachment; filename="'. rawurlencode($filename) .'"');

if ($httpStatus === 206) {

    header("Content-Range: bytes $start-$end/$size");

}

header("Content-Length: $length");


// 定位并输出

if ($start > 0) { fseek($fp, $start); }

$sent = 0; $buf = 8192;

while (!feof($fp) && $sent < $length) {

    $need = min($buf, $length - $sent);

    $chunk = fread($fp, $need);

    if ($chunk === false) break;

    echo $chunk;

    $sent += strlen($chunk);

    if (connection_aborted()) break;

}


fclose($fp);

log_download($pdo, $fileId, $userId, $sent);

```


---


## 7) 简单资源列表(分页)——`list.php`


```php

<?php

require __DIR__ . '/db.php';

require __DIR__ . '/utils.php';

$userId = current_user_id();


$page = max(1, (int)($_GET['page'] ?? 1));

$ps = 20; $off = ($page-1)*$ps;


$sql = "SELECT id, original_name, kind, mime_type, size_bytes, uploaded_at, vip_required FROM media_files

        WHERE user_id=? AND deleted_at IS NULL ORDER BY id DESC LIMIT $ps OFFSET $off";

$rows = $pdo->prepare($sql); $rows->execute([$userId]); $rows = $rows->fetchAll();

?>

<!doctype html><html><head><meta charset="utf-8"><title>我的资源</title></head><body>

<h1>我的资源</h1>

<p><a href="upload_form.php">上传新文件</a></p>

<table border="1" cellpadding="6" cellspacing="0">

<tr><th>ID</th><th>文件名</th><th>类型</th><th>MIME</th><th>大小</th><th>时间</th><th>VIP</th><th>下载</th></tr>

<?php foreach ($rows as $r): ?>

<tr>

  <td><?= htmlspecialchars($r['id']) ?></td>

  <td><?= htmlspecialchars($r['original_name']) ?></td>

  <td><?= htmlspecialchars($r['kind']) ?></td>

  <td><?= htmlspecialchars($r['mime_type']) ?></td>

  <td><?= number_format($r['size_bytes']) ?></td>

  <td><?= htmlspecialchars($r['uploaded_at']) ?></td>

  <td><?= $r['vip_required'] ? '是' : '否' ?></td>

  <td><a href="download.php?id=<?= (int)$r['id'] ?>">下载</a></td>

</tr>

<?php endforeach; ?>

</table>

</body></html>

```


---


## 8) Nginx/PHP 注意点


* **上传体积**:确保 `client_max_body_size`(Nginx)与 `post_max_size`、`upload_max_filesize`(PHP)足够大。

* **超时**:大文件上传适当放宽 `fastcgi_read_timeout`、`max_execution_time`。

* **Range**:`download.php` 已支持 `Range`;若前面还有反向代理,确保未去除 `Range` 头。


---


## 9) 可选增强


* 生成图片/视频 **低清预览**(`is_public=1`)与防直链签名 URL;

* 元数据抽取(FFprobe/ExifTool);

* 风控:同一 IP/用户下载频率限制;

* 全文检索(`FULLTEXT` + 中文分词)。


---


> 到这里就是“可直接跑”的最小闭环:上传 -> SMB 入库;下载 -> VIP/授权校验 + Range 断点。按你的环境把 `config.php` 中数据库与 SMB 账号改一下即可。


2025-9-18 20:59#1
View profile  Blog  Send a short message  Top
     


  Printable version | Recommend to a friend | Subscribe to topic | Favorite topic  


 


All times are GMT+8, and the current time is 2026-1-14 04:09 Clear informations ->sessions/cookies - Contact Us - CAFFZ - ZAKE