## GitHub 同步说明 | 项 | 内容 | |:---|:---| | 仓库 | https://github.com/fnvtk/cunkebao_v3 | | 分支 | develop | | 方向 | 本地 → GitHub(单向 push) | | 是否拉取远程 | 否(未 fetch / pull / merge) | | 是否改本地源文件 | 否(仅 git add/commit,未编辑业务/文档正文) | | 远程基准 | origin/develop @b5b40e8b| | 本批工作区变更 | 999 files, +47622 / -4007 lines | ## 一并推送的本地已有 commit(此前未 push) 1.3aa7ecf8deploy: 官网 ckb.quwanzhi.com 宝塔上线 DNS+SSL+验收文档 2.16a70045fix(官网): 桌面导航仅保留一个留资 CTA 并重新上线 3.dbe7b7aafix(官网): Hero 改为主 CTA 按钮并去掉重复 1200+ 数据条 4.ac82726echore(部署): 五端上线验收46项并统一留资与缓存版本 ## 本 commit 目录统计 | 目录 | 文件数 | 说明 | |:---|---:|:---| | 开发文档/ | 639 | 10、项目管理、未完成项跨仓汇总、五端需求/部署/接口 | | Server/ | 129 | 后端 API、迁移、部署相关 | | Touchkebao/ | 80 | PC 工作台、获客、算力 | | 官网/ | 61 | ckb.quwanzhi.com 静态站与 CTA 迭代 | | Cunkebao/ | 49 | 移动端场景/设置/流量池 | | SuperAdmin/ | 26 | 超管后台 | | .cursor/ | 11 | 规则与 Skill | | 其他 | 14 | docker-compose、.obsidian、.codegraph 等 | ## 重点文件(本地为准) - 开发文档/1、需求/修改/未完成项与跨仓验收汇总.md - 开发文档/10、项目管理/账号密码与登录环境一览.md - 开发文档/10、项目管理/(新建项目管理目录与截图) - 官网/ 全站 + 宝塔上线文档 - 五端上线验收 46 项(见 ac82726e) ## 刻意未上传 - node_modules/、.DS_Store、.smart-env/ - .开发文档_nested_git_backup/ - **/__pycache__/ ## 验收 - 提交页(含本说明): https://github.com/fnvtk/cunkebao_v3/commit/HEAD - 分支树: https://github.com/fnvtk/cunkebao_v3/tree/develop Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
5.9 KiB
PHP
188 lines
5.9 KiB
PHP
<?php
|
||
namespace app\common\controller;
|
||
|
||
use think\Controller;
|
||
use think\facade\Request;
|
||
use app\common\model\Attachment as AttachmentModel;
|
||
use app\common\util\AliyunOSS;
|
||
|
||
class Attachment extends Controller
|
||
{
|
||
/**
|
||
* 上传文件
|
||
* @return \think\response\Json
|
||
*/
|
||
public function upload()
|
||
{
|
||
try {
|
||
// 获取上传文件
|
||
$file = Request::file('file');
|
||
if (!$file) {
|
||
return json([
|
||
'code' => 400,
|
||
'msg' => '请选择要上传的文件'
|
||
]);
|
||
}
|
||
|
||
// 验证文件
|
||
$validate = \think\facade\Validate::rule([
|
||
'file' => [
|
||
'fileSize' => 50485760, // 50MB
|
||
'fileExt' => 'jpg,jpeg,png,gif,webp,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls,ppt,pptx,txt',
|
||
]
|
||
]);
|
||
|
||
if (!$validate->check(['file' => $file])) {
|
||
return json([
|
||
'code' => 400,
|
||
'msg' => $validate->getError()
|
||
]);
|
||
}
|
||
|
||
// 生成文件hash
|
||
$hashKey = md5_file($file->getRealPath());
|
||
|
||
// 检查文件是否已存在
|
||
$existFile = AttachmentModel::getByHashKey($hashKey);
|
||
if ($existFile) {
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => '文件已存在',
|
||
'data' => [
|
||
'id' => $existFile['id'],
|
||
'name' => $existFile['name'],
|
||
'url' => $existFile['source'],
|
||
'size' => isset($existFile['size']) ? $existFile['size'] : 0
|
||
]
|
||
]);
|
||
}
|
||
|
||
|
||
|
||
// 生成OSS对象名称
|
||
$objectName = AliyunOSS::generateObjectName($file->getInfo('name'));
|
||
|
||
// 上传到OSS
|
||
$result = AliyunOSS::uploadFile($file->getRealPath(), $objectName);
|
||
|
||
// OSS 不可用时回落本地 public/uploads(Docker 本地开发)
|
||
if (!$result['success']) {
|
||
$local = $this->saveLocalUpload($file, $hashKey);
|
||
if ($local) {
|
||
$result = $local;
|
||
} else {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => '文件上传失败:' . ($result['error'] ?? 'OSS 不可用')
|
||
]);
|
||
}
|
||
}
|
||
|
||
if (empty($result['success'])) {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => '文件上传失败'
|
||
]);
|
||
}
|
||
|
||
// 保存到数据库
|
||
$attachmentData = [
|
||
'name' => Request::param('name') ?: $file->getInfo('name'),
|
||
'hash_key' => $hashKey,
|
||
'server' => 'aliyun_oss',
|
||
'source' => $result['url'],
|
||
'size' => $result['size'],
|
||
'suffix' => pathinfo($file->getInfo('name'), PATHINFO_EXTENSION),
|
||
// 兼容 ck_attachments.scene 非空约束:默认获客海报场景
|
||
'scene' => (int)Request::param('scene', 1),
|
||
];
|
||
|
||
$attachment = AttachmentModel::addAttachment($attachmentData);
|
||
|
||
if (!$attachment) {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => '保存附件信息失败'
|
||
]);
|
||
}
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => '上传成功',
|
||
'data' => [
|
||
'id' => $attachment->id,
|
||
'name' => $attachmentData['name'],
|
||
'url' => $attachmentData['source'],
|
||
'size' => $attachmentData['size']
|
||
]
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => '上传失败:' . $e->getMessage()
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 本地存储回落(OSS 失败 / 离线 Docker)
|
||
*/
|
||
private function saveLocalUpload($file, string $hashKey): ?array
|
||
{
|
||
try {
|
||
$subdir = date('Y/m/d');
|
||
$dir = root_path() . 'public/uploads/' . $subdir;
|
||
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
|
||
return null;
|
||
}
|
||
$origName = $file->getInfo('name') ?: 'upload.bin';
|
||
$ext = pathinfo($origName, PATHINFO_EXTENSION) ?: 'bin';
|
||
$filename = $hashKey . '.' . $ext;
|
||
$target = $dir . '/' . $filename;
|
||
if (!copy($file->getRealPath(), $target)) {
|
||
return null;
|
||
}
|
||
$relative = '/uploads/' . $subdir . '/' . $filename;
|
||
$domain = rtrim(Request::domain(), '/');
|
||
return [
|
||
'success' => true,
|
||
'url' => $domain . $relative,
|
||
'size' => filesize($target),
|
||
];
|
||
} catch (\Throwable $e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取附件信息
|
||
* @param int $id 附件ID
|
||
* @return \think\response\Json
|
||
*/
|
||
public function info($id)
|
||
{
|
||
try {
|
||
$attachment = AttachmentModel::find($id);
|
||
|
||
if (!$attachment) {
|
||
return json([
|
||
'code' => 404,
|
||
'msg' => '附件不存在'
|
||
]);
|
||
}
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => '获取成功',
|
||
'data' => $attachment
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => '获取失败:' . $e->getMessage()
|
||
]);
|
||
}
|
||
}
|
||
}
|