Files
Manus AI 2753265edb sync(本地→GitHub): 2026-06-03 21:17 全量单向同步
## 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. 3aa7ecf8 deploy: 官网 ckb.quwanzhi.com 宝塔上线 DNS+SSL+验收文档
2. 16a70045 fix(官网): 桌面导航仅保留一个留资 CTA 并重新上线
3. dbe7b7aa fix(官网): Hero 改为主 CTA 按钮并去掉重复 1200+ 数据条
4. ac82726e chore(部署): 五端上线验收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>
2026-06-03 21:17:41 +08:00

188 lines
5.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<?php
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/uploadsDocker 本地开发)
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()
]);
}
}
}