初始化
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
APP_NAME=CloudChip
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://cloud-chip.cn
|
||||
|
||||
# 宝塔 PHP8.3 + MySQL5.7 适配:collation 必须用 utf8mb4_unicode_ci(5.7 不支持 0900_ai_ci)
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=cloudchip_cn
|
||||
DB_USERNAME=cloudchip_cn
|
||||
DB_PASSWORD=
|
||||
DB_CHARSET=utf8mb4
|
||||
DB_COLLATION=utf8mb4_unicode_ci
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_SECURE_COOKIE=true
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=warning
|
||||
|
||||
# 宝塔 Redis 若已安装可改用 redis,默认 file 即可跑通
|
||||
# CACHE_DRIVER=redis
|
||||
# SESSION_DRIVER=redis
|
||||
# REDIS_HOST=127.0.0.1
|
||||
# REDIS_PASSWORD=null
|
||||
# REDIS_PORT=6379
|
||||
+19
-13
@@ -1,13 +1,19 @@
|
||||
# ---> AL
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
*.app
|
||||
.snapshots/*
|
||||
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
*.zip
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 部署指南(深圳市云创芯电子有限公司 · Laravel 12)
|
||||
|
||||
本包已包含 `vendor/`(Composer 依赖已安装),上传到服务器后**无需再跑 composer install** 即可运行。
|
||||
如需自行重装依赖,删除 `vendor/` 后执行 `composer install --no-dev --optimize-autoloader`。
|
||||
|
||||
## 1. 上传与解压
|
||||
- 将 `cloud-chip-laravel-vue.tar.gz` 上传到服务器网站根目录(如 Baota 的 `/www/wwwroot/cloud-chip.cn/`)。
|
||||
- 解压:
|
||||
```bash
|
||||
tar -xzf cloud-chip-laravel-vue.tar.gz -C /www/wwwroot/cloud-chip.cn/
|
||||
```
|
||||
|
||||
## 2. 配置环境变量
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 用编辑器打开 .env,至少修改以下项:
|
||||
# APP_URL=https://你的域名
|
||||
# DB_DATABASE / DB_USERNAME / DB_PASSWORD
|
||||
# APP_DEBUG=false
|
||||
# SESSION_SECURE_COOKIE=true # 启用了 HTTPS 才设 true
|
||||
```
|
||||
生成应用密钥:
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
## 3. 数据库
|
||||
```bash
|
||||
php artisan migrate --force
|
||||
# 如需初始演示数据(产品/文章/术语等):
|
||||
php artisan db:seed --force
|
||||
```
|
||||
> 本次发布包含新增迁移 `2026_08_02_000001_add_profile_fields_to_users_table`:
|
||||
> 在 `users` 表追加 `real_name / phone / company / industry` 四个资料字段(均允许为空)。
|
||||
> **首次部署或升级时务必执行 `migrate --force`**,否则详细注册收集到的字段无法落库。
|
||||
|
||||
## 4. 存储软链(文章图片上传需要)
|
||||
```bash
|
||||
php artisan storage:link
|
||||
```
|
||||
这会把 `storage/app/public` 映射到 `public/storage`,使上传的图片可通过网页访问。
|
||||
|
||||
## 5. 目录权限(Baota 面板或命令行)
|
||||
确保以下目录**可写**(命令行示例):
|
||||
```bash
|
||||
chmod -R 755 storage bootstrap/cache
|
||||
# 若仍报权限错,再放宽到 775 / 777(仅限这两个目录)
|
||||
```
|
||||
> 文章配图、封面图通过 `/upload` 接口写入 `storage/app/public/uploads/{年}/{月}/`,
|
||||
> 该目录由上传时自动创建,且已随包带 `storage/app/public/.gitkeep`。
|
||||
> 上一步 `chmod 755 storage` 已覆盖此路径;若上传报「无法写入」,单独放宽:
|
||||
> `chmod -R 755 storage/app/public`
|
||||
|
||||
## 6. Web 服务器(关键:docroot 必须是 public/)
|
||||
- **Nginx**:`root` 指向 `.../public;` 并加 Laravel 重写:
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
```
|
||||
- **Apache**:本包已带 `public/.htaccess`,只要把站点根目录设为 `public/` 即可,无需额外配置。
|
||||
- ⚠️ **不要**用项目根的 `router.php` 跑生产(`router.php` 仅本地 `php -S` 调试用)。
|
||||
|
||||
## 7. 上线检查
|
||||
- 访问首页、产品、文章、论坛、联系我们页均返回 200。
|
||||
- 后台入口 `/admin`(需管理员账号,任意后台角色可登录)。
|
||||
- 前台用户注册/登录在「论坛」页内;管理员登录在独立页面 `/admin/login`。
|
||||
|
||||
## 8. SEO 与搜索引擎收录(sitemap / robots)
|
||||
上线后建议提交站点地图,加快收录:
|
||||
- **robots 协议**:访问 `/robots.txt` 自动返回(动态读取 `APP_URL`,无需手改域名),
|
||||
已对前台全开放,并屏蔽 `/admin`、`/login`、`/register`、`/settings`、`/upload`、`/api` 等后台与用户接口。
|
||||
- **站点地图**:访问 `/sitemap.xml`(机读 XML,含首页/关于我们/产品服务/文章/术语库/论坛/联系我们 + 全部已发布产品、文章、术语)。
|
||||
- **提交地址**:
|
||||
- 百度搜索资源平台:https://ziyuan.baidu.com/ → 站点管理 → 链接提交 → 自动提交(sitemap)填入 `https://你的域名/sitemap.xml`
|
||||
- Google Search Console:https://search.google.com/search-console → Sitemaps 填入 `https://你的域名/sitemap.xml`
|
||||
- 上线检查项补一条:访问 `/robots.txt` 与 `/sitemap.xml` 均返回 200,且 `Sitemap:` 行域名与 `APP_URL` 一致。
|
||||
|
||||
## 备注
|
||||
- 图标采用 x-icon(纯 SVG,无 emoji);配色走 CSS 变量 token,未硬编码颜色。
|
||||
- **本次已上线功能**:
|
||||
- 文章富文本编辑(Quill)+ 图片上传(/upload 接口)+ HTMLPurifier 净化,正文安全渲染 HTML。
|
||||
- 详细注册:注册表单新增「真实姓名 / 手机号 / 公司名称 / 行业领域」四项(选填)。
|
||||
- 会员等级徽章(`level-badge` 组件):论坛帖子与回复署名、导航(桌面+移动端)、账户设置页均展示等级。
|
||||
- **仍待办**:微信扫码注册(本期未排期,暂未实现)。
|
||||
@@ -1,3 +1,153 @@
|
||||
# cloud-chip.cn
|
||||
# 深圳市云创芯电子有限公司 · Laravel 12 + Vue3(CDN) 官网
|
||||
|
||||
pd网站
|
||||
一套**零构建、零 Node**的轻量全栈站点,专为宝塔面板(Nginx + PHP 8.3 + MySQL 5.7)设计,强调安全、稳定、易移植。
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 前台 | 后台管理 |
|
||||
|------|------|----------|
|
||||
| 产品中心 | 列表 + 搜索 + 分类筛选 + 详情页 | CRUD + 分类 + 草稿/发布 |
|
||||
| 技术文章 | 列表 + 搜索 + 分类筛选 + 详情页 | CRUD + 分类 + 作者 + 发布时间 |
|
||||
| 术语库 | 列表 + 搜索 + 详情页 | CRUD + 别名 + 分类 |
|
||||
| 技术论坛 | 版块 + 帖子 + 回复 + 发帖/回复 | 版块管理 + 帖子删除 + 锁帖 |
|
||||
| 联系我们 | Vue 异步表单 + CSRF + 限流 | 留言列表 + 详情 + 删除 |
|
||||
| 用户系统 | 注册 + 登录 + 退出 + 个人设置 | 用户列表 + 管理员提权/降权 + 删除 |
|
||||
| 用户等级 | 积分驱动 L1–L5 徽章 + 进度条 | 用户列表可见积分 |
|
||||
| 账号安全 | `/settings` 自助修改密码(需验证当前密码) | - |
|
||||
| 站点设置 | 站名 / Logo / Favicon / ICP 全站生效 | `/admin/settings` 统一维护 |
|
||||
| SEO | 每页独立 title + description,缺省回落全局 | 文章/产品表单内逐条填写 |
|
||||
| 文章版式 | 三种阅读版式(标准 / 宽栏 / 文档) | 发布时下拉选择 |
|
||||
| 仪表盘 | - | 统计概览 + 最新留言/帖子 |
|
||||
|
||||
## 技术选型
|
||||
|
||||
| 项 | 方案 |
|
||||
|----|------|
|
||||
| 后端 | Laravel 12(PHP 8.3) |
|
||||
| 前端交互 | Vue 3.5.13 全局构建,本地自托管(无 CDN 依赖) |
|
||||
| 样式 | 纯 CSS + 设计令牌(:root 变量),全站只用 var() |
|
||||
| 视图 | Blade 服务端渲染(SEO 友好,首屏快) |
|
||||
| 图标 | 内联 SVG 组件(统一描边,16/20/24 三档尺寸) |
|
||||
| 前端构建 | **无构建**(无 npm / Vite / Tailwind) |
|
||||
| 数据库 | MySQL 5.7(utf8mb4_unicode_ci) |
|
||||
| RBAC | is_admin 布尔标志位 + Admin 中间件 + /admin 路由组 |
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
app/
|
||||
Http/Controllers/
|
||||
HomeController.php 首页(产品+文章)
|
||||
ProductController.php 产品列表/详情
|
||||
ArticleController.php 文章列表/详情
|
||||
GlossaryController.php 术语列表/详情
|
||||
ForumController.php 论坛版块/帖子/回复
|
||||
ContactController.php 联系表单(Vue 异步 + 限流)
|
||||
HealthController.php /api/health 健康检查
|
||||
Auth/LoginController.php 登录 / 退出
|
||||
Auth/RegisterController.php 注册
|
||||
Auth/SettingsController.php 个人设置(修改密码)
|
||||
Admin/ 后台管理(10 个控制器,含站点设置)
|
||||
Http/Middleware/Admin.php 管理员中间件
|
||||
Models/ 10 个模型(含关系和 scope)
|
||||
database/
|
||||
migrations/ 16 个迁移
|
||||
seeders/ 7 个 seeder(全部幂等,可重复执行)
|
||||
public/
|
||||
css/app.css 设计令牌 + 完整组件样式
|
||||
js/app.js Vue 挂载(联系表单)
|
||||
js/vendor/vue.global.prod.js 自托管 Vue 3.5.13
|
||||
resources/views/
|
||||
layouts/app.blade.php 前台布局
|
||||
components/icon.blade.php 内联 SVG 图标组件
|
||||
home.blade.php 首页
|
||||
products/ articles/ glossary/ 前台展示页
|
||||
forum/ 论坛页面
|
||||
auth/ 登录/注册/个人设置
|
||||
admin/ 后台管理页面(layout + 16 个子页面)
|
||||
routes/web.php 前台 + /admin 路由组
|
||||
```
|
||||
|
||||
## 安全要点
|
||||
|
||||
- 全站 CSRF(`@csrf` + 前端 `X-CSRF-TOKEN`)
|
||||
- 表单服务端校验(`validate`),Vue 仅做体验增强
|
||||
- 联系表单频率限制:同一 IP 60 秒内最多 5 次
|
||||
- 密码 `bcrypt`(`hashed` cast),会话 cookie `secure` + `http_only` + `encrypt`
|
||||
- Blade `{{ }}` 自动转义,防 XSS
|
||||
- Eloquent 参数化查询,防 SQL 注入
|
||||
- Admin 中间件保护所有后台路由
|
||||
|
||||
## 宝塔部署(3 步)
|
||||
|
||||
### 第 1 步:建站点 + 数据库
|
||||
|
||||
宝塔 → 网站 → 添加站点:
|
||||
- 根目录指向 `public/`
|
||||
- PHP 版本:8.3
|
||||
- 同时建好 MySQL 数据库(排序规则 `utf8mb4_unicode_ci`)
|
||||
|
||||
### 第 2 步:传代码 + 安装
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/cloud-chip.cn
|
||||
|
||||
# 配置 Composer 镜像(国内服务器)
|
||||
composer config -g repos.packagist.org composer https://mirrors.aliyun.com/composer/
|
||||
|
||||
# 安装依赖(必须先于 artisan 命令)
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
|
||||
# 配置环境
|
||||
cp .env.example .env
|
||||
# 编辑 .env,填写:
|
||||
# DB_DATABASE=你的数据库名
|
||||
# DB_USERNAME=你的数据库用户
|
||||
# DB_PASSWORD=你的数据库密码
|
||||
# APP_URL=https://cloud-chip.cn
|
||||
|
||||
# 生成密钥 + 迁移 + 种子数据
|
||||
php artisan key:generate
|
||||
php artisan migrate --force
|
||||
php artisan db:seed --force
|
||||
|
||||
# 权限
|
||||
chmod -R 755 storage bootstrap/cache
|
||||
php artisan storage:link
|
||||
```
|
||||
|
||||
或直接运行 `./install.sh`(自动完成上述全部步骤)。
|
||||
|
||||
### 第 3 步:配置伪静态
|
||||
|
||||
宝塔 → 站点设置 → 伪静态 → 选择 **Laravel** 模板。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- **宝塔禁用函数**:PHP-8.3 设置 → 禁用函数,删除 `putenv, proc_open, proc_get_status, proc_close, popen` 并重启 PHP-FPM
|
||||
- **运行时目录**:确保 `storage/` 和 `bootstrap/cache/` 对 www 用户可写(755)
|
||||
|
||||
## 默认管理员账号
|
||||
|
||||
Seeder 会自动创建管理员账号:
|
||||
- 邮箱:`admin@cloud-chip.cn`
|
||||
- 密码:`cloudchip2026`
|
||||
|
||||
**请在首次登录后立即修改密码**:登录后访问 `/settings` → 修改密码(需输入当前密码校验)。
|
||||
|
||||
## 数据库表
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| users | 用户(is_admin 布尔 + points 积分) |
|
||||
| settings | 站点级 key-value 配置(ICP / Logo / SEO 等) |
|
||||
| password_reset_tokens | 密码重置令牌 |
|
||||
| failed_jobs | 失败任务队列 |
|
||||
| contact_messages | 联系表单留言 |
|
||||
| categories | 分类(type: product/article) |
|
||||
| products | 产品(软删) |
|
||||
| articles | 文章(软删 + 发布时间) |
|
||||
| glossary_terms | 术语 |
|
||||
| forum_boards | 论坛版块 |
|
||||
| forum_threads | 论坛帖子(软删 + 置顶 + 锁定) |
|
||||
| forum_replies | 论坛回复(软删) |
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# 企业官网 · 安全等级评估报告
|
||||
|
||||
> 评估对象:cloud-chip-laravel-vue(Laravel 12 + Vue3 CDN 零构建栈)
|
||||
> 评估日期:2026-08-03
|
||||
> 评估方式:源码审计 + 本地运行验证(PHP 8.3 / MySQL 5.7),覆盖路由、控制器、中间件、配置、视图与依赖。
|
||||
|
||||
---
|
||||
|
||||
## 一、综合评级
|
||||
|
||||
| 场景 | 综合等级 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 当前(本地开发配置) | **B** | 框架与代码安全基线良好;但 `.env` 仍是开发值(`APP_DEBUG=true`、`SESSION_SECURE_COOKIE=false`),且此前缺少响应头与登录节流(本次已修复)。 |
|
||||
| 部署合规后(修正配置 + HTTPS) | **A‑** | 达到企业官网安全基线。剩余项均为可选增强。 |
|
||||
|
||||
> 评级说明:本项目基于 Laravel 12「安全优先」框架,代码层已具备 CSRF、参数化查询、输出转义、严格上传校验等能力;风险主要集中在**部署配置**与**纵深防御响应头**,均已给出整改项。
|
||||
|
||||
---
|
||||
|
||||
## 二、各安全维度评分
|
||||
|
||||
| 维度 | 评级 | 关键依据 |
|
||||
| --- | --- | --- |
|
||||
| 注入防护(SQLi / XSS) | **A** | Eloquent 全程参数化查询,杜绝 SQL 注入;视图默认 `{{ }}` 转义;富文本经 `mews/purifier` 净化(default/article 两档)。 |
|
||||
| 身份认证与会话 | **A‑** | 密码 bcrypt 哈希;登录成功 `session()->regenerate()` 防会话固定;新增登录节流(5 次/分钟);后台具备 RBAC 五角色 + 权限点门禁。 |
|
||||
| Cookie 安全 | **B+** | `http_only=true`、`encrypt=true`、`same_site=lax` 均已开启;`secure` 在 `.env` 中为 `false`(本地值,上线须置 `true`)。 |
|
||||
| 配置安全 | **C+** | 当前 `.env`:`APP_DEBUG=true`、`APP_ENV=local`、数据库弱口令 `cloudchip_cn/cloudchip_cn`——**均为本地开发值,上线前必须修改**。 |
|
||||
| 传输与响应头 | **A‑** | 本次新增 `SecurityHeaders` 中间件:CSP、X-Frame-Options(DENY)、X-Content-Type-Options(nosniff)、Referrer-Policy、Permissions-Policy;HSTS 待生产 HTTPS 启用。 |
|
||||
| 文件上传 | **A** | `image` + `mimes:jpg,jpeg,png,gif,webp` + `max:5120` 严格校验;存储于 `storage/app/public`(经 `storage:link` 暴露,非 PHP 执行目录),命名随机防遍历。 |
|
||||
| 防滥用 / 限流 | **A‑** | 联系表单 `RateLimiter` 5 次/60 秒;登录接口新增 `ThrottlesLogins` 5 次/分钟(按 IP+邮箱)。 |
|
||||
| 依赖与供应链 | **B** | 仅引入 `mews/purifier`、`simple-qrcode` 等少量可信依赖;建议上线前执行 `composer audit`。 |
|
||||
| 日志与错误处理 | **B+** | `LOG_CHANNEL=stack`;只要 `APP_DEBUG=false`,异常不会向用户泄露堆栈与环境信息。 |
|
||||
|
||||
---
|
||||
|
||||
## 三、本次已实施的安全加固(代码层)
|
||||
|
||||
1. **新增 `App\Http\Middleware\SecurityHeaders` 并注册到 `web` 中间件组**
|
||||
- 下发 `Content-Security-Policy`(限制脚本/样式/连接/表单来源,显著缩小 XSS 影响面)、`X-Frame-Options: DENY`(防点击劫持)、`X-Content-Type-Options: nosniff`(防 MIME 嗅探)、`Referrer-Policy`、`Permissions-Policy`。
|
||||
- 生产 + HTTPS 环境下自动追加 `Strict-Transport-Security`(HSTS,含 `preload`)。
|
||||
- CSP 针对本项目的零构建架构做了适配:放行 `cdn.tailwindcss.com` 与内联脚本/样式(否则 Tailwind Play CDN 与内联 `tailwind.config` 会被拦截)。
|
||||
|
||||
2. **登录接口增加防暴力破解节流**
|
||||
- `LoginController` 引入 `ThrottlesLogins`,同一 IP+邮箱 每分钟最多 5 次,超限返回校验错误并锁定。
|
||||
|
||||
3. **修复联系表单 CSRF 失效缺陷(功能性 + 安全性)**
|
||||
- 发现布局缺少 `<meta name="csrf-token">`,导致 Vue 联系表单取不到 token、提交被 Laravel 拦截返回 419(原表单实际不可用)。
|
||||
- 已在布局 `<head>` 补齐该 meta 标签,并经端到端验证:取 token → POST → 返回 `{"ok":true}`,HTTP 200。
|
||||
|
||||
---
|
||||
|
||||
## 四、残余风险与整改建议(按优先级)
|
||||
|
||||
### P0 · 上线前必须完成
|
||||
- [ ] **关闭调试模式**:`.env` 设 `APP_DEBUG=false`、`APP_ENV=production`。否则将向访客泄露堆栈、环境变量等敏感信息。
|
||||
- [ ] **启用安全 Cookie**:`SESSION_SECURE_COOKIE=true`(HTTPS 站点必须),防止会话被明文中间人窃取。
|
||||
- [ ] **强制 HTTPS**:在反向代理/Web 服务器层 301 跳转 HTTP→HTTPS,并启用 HSTS(中间件已在生产自动下发)。
|
||||
- [ ] **数据库强口令 + 最小权限**:更换为高强度独立口令,账号仅授予本站库所需权限;切勿使用本地弱口令 `cloudchip_cn`。
|
||||
- [ ] **保护敏感文件**:确保 `.env`、`.gitignore`、`.workbuddy` 等不被 Web 直接访问(当前项目结构 `.env` 在 `public/` 之外,已天然隔离;部署时复核 Nginx/Apache 配置,禁止访问点号开头的隐藏文件)。
|
||||
|
||||
### P1 · 强烈建议
|
||||
- [ ] **配置可信代理 `TrustProxies`**:若站点置于 CDN / 负载均衡之后,需在 `bootstrap/app.php` 声明可信代理,否则 `secure` cookie 与 `isSecure()` 会误判,影响 HTTPS 与会话安全。
|
||||
- [ ] **依赖审计**:上线前执行 `composer audit` 并升级存在 CVE 的包。
|
||||
- [ ] **后台安全增强**:管理员登录增加失败告警 / 锁定,条件允许时启用 2FA;对 `/admin` 异常访问做监控。
|
||||
- [ ] **异常监控**:对批量联系提交、异常登录、5xx 突增配置告警。
|
||||
|
||||
### P2 · 可选增强
|
||||
- [ ] **收紧 CSP**:将 Tailwind 改为构建产物 + 为内联脚本加 `nonce`,从而移除 `unsafe-inline`,把 XSS 防御从「依赖转义」升级为「双层防护」。
|
||||
- [ ] **合规页面**:补充 `security.txt`、隐私政策与备案信息展示(页脚已预留 ICP / 公安备案位)。
|
||||
- [ ] **外部资源 SRI**:若后续引入任何第三方脚本/样式,添加子资源完整性校验。
|
||||
|
||||
---
|
||||
|
||||
## 五、部署上线前必检清单
|
||||
|
||||
| 检查项 | 命令 / 位置 | 期望 |
|
||||
| --- | --- | --- |
|
||||
| 调试关闭 | `.env` `APP_DEBUG` | `false` |
|
||||
| 环境 | `.env` `APP_ENV` | `production` |
|
||||
| Cookie 安全 | `.env` `SESSION_SECURE_COOKIE` | `true` |
|
||||
| APP_KEY | `php artisan key:generate`(如未生成) | 已设置且唯一 |
|
||||
| 路由缓存 | `php artisan route:cache` | 无冲突 |
|
||||
| 视图缓存 | `php artisan view:cache` | 编译通过 |
|
||||
| 数据库迁移 | `php artisan migrate --force` | 成功 |
|
||||
| 依赖审计 | `composer audit` | 无高危 |
|
||||
| HTTPS | Web 服务器 / CDN | 已启用 + 跳转 |
|
||||
| 静态资源软链 | `php artisan storage:link` | `/storage` 可访问 |
|
||||
|
||||
---
|
||||
|
||||
## 六、验证记录(本地)
|
||||
|
||||
| 页面 | 路由 | 状态码 |
|
||||
| --- | --- | --- |
|
||||
| 首页 | `/` | 200 |
|
||||
| 关于我们 | `/about` | 200 |
|
||||
| 产品服务 | `/products` | 200 |
|
||||
| 联系我们 | `/contact` | 200 |
|
||||
| 联系表单提交(带 CSRF) | `POST /contact` | 200 `{"ok":true}` |
|
||||
| 安全响应头 | 所有页面 | CSP / XFO / NoSniff / Referrer / Permissions 均已下发 |
|
||||
|
||||
---
|
||||
|
||||
*报告由 WorkBuddy 基于真实源码与本地运行结果生成,非泛泛模板。所有 P0 项均为部署配置,不依赖代码改动即可完成。*
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* The list of the inputs that are never flashed to the session on validation errors.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Article;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ArticleController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$articles = Article::with('category')->orderByDesc('created_at')->paginate(20);
|
||||
|
||||
return view('admin.articles.index', compact('articles'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$categories = Category::ofType('article')->orderBy('sort')->orderBy('name')->get();
|
||||
|
||||
return view('admin.articles.form', ['article' => null, 'categories' => $categories]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'category_id' => ['nullable', 'exists:categories,id'],
|
||||
'title' => ['required', 'string', 'max:160'],
|
||||
'slug' => ['required', 'string', 'max:160', 'unique:articles,slug'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'body' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'author' => ['nullable', 'string', 'max:60'],
|
||||
'status' => ['required', 'in:approved,draft'],
|
||||
'template' => ['required', 'in:standard,tech,feature'],
|
||||
'seo_title' => ['nullable', 'string', 'max:160'],
|
||||
'seo_description' => ['nullable', 'string', 'max:255'],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
if (! empty($data['body'])) {
|
||||
$data['body'] = \Purifier::clean($data['body'], 'article');
|
||||
}
|
||||
|
||||
if (empty($data['published_at']) && $data['status'] === 'approved') {
|
||||
$data['published_at'] = now();
|
||||
}
|
||||
|
||||
Article::create($data);
|
||||
|
||||
return redirect()->route('admin.articles.index')->with('success', '文章已创建');
|
||||
}
|
||||
|
||||
public function edit(Article $article): View
|
||||
{
|
||||
$categories = Category::ofType('article')->orderBy('sort')->orderBy('name')->get();
|
||||
|
||||
return view('admin.articles.form', compact('article', 'categories'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Article $article)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'category_id' => ['nullable', 'exists:categories,id'],
|
||||
'title' => ['required', 'string', 'max:160'],
|
||||
'slug' => ['required', 'string', 'max:160', 'unique:articles,slug,' . $article->id],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'body' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'author' => ['nullable', 'string', 'max:60'],
|
||||
'status' => ['required', 'in:approved,draft'],
|
||||
'template' => ['required', 'in:standard,tech,feature'],
|
||||
'seo_title' => ['nullable', 'string', 'max:160'],
|
||||
'seo_description' => ['nullable', 'string', 'max:255'],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
if (! empty($data['body'])) {
|
||||
$data['body'] = \Purifier::clean($data['body'], 'article');
|
||||
}
|
||||
|
||||
if (empty($data['published_at']) && $data['status'] === 'approved' && $article->published_at === null) {
|
||||
$data['published_at'] = now();
|
||||
}
|
||||
|
||||
$article->update($data);
|
||||
|
||||
return redirect()->route('admin.articles.index')->with('success', '文章已更新');
|
||||
}
|
||||
|
||||
public function destroy(Article $article)
|
||||
{
|
||||
$article->delete();
|
||||
|
||||
return redirect()->route('admin.articles.index')->with('success', '文章已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$categories = Category::orderBy('type')->orderBy('sort')->orderBy('name')->paginate(20);
|
||||
|
||||
return view('admin.categories.index', compact('categories'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.categories.form', ['category' => null]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:60'],
|
||||
'slug' => ['required', 'string', 'max:80', 'unique:categories,slug'],
|
||||
'type' => ['required', 'in:product,article'],
|
||||
'parent_id' => ['nullable', 'integer'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
Category::create($data);
|
||||
|
||||
return redirect()->route('admin.categories.index')->with('success', '分类已创建');
|
||||
}
|
||||
|
||||
public function edit(Category $category): View
|
||||
{
|
||||
return view('admin.categories.form', compact('category'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Category $category)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:60'],
|
||||
'slug' => ['required', 'string', 'max:80', 'unique:categories,slug,' . $category->id],
|
||||
'type' => ['required', 'in:product,article'],
|
||||
'parent_id' => ['nullable', 'integer'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
$category->update($data);
|
||||
|
||||
return redirect()->route('admin.categories.index')->with('success', '分类已更新');
|
||||
}
|
||||
|
||||
public function destroy(Category $category)
|
||||
{
|
||||
$category->delete();
|
||||
|
||||
return redirect()->route('admin.categories.index')->with('success', '分类已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ContactMessage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ContactController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$messages = ContactMessage::orderByDesc('created_at')->paginate(20);
|
||||
|
||||
return view('admin.contacts.index', compact('messages'));
|
||||
}
|
||||
|
||||
public function show(ContactMessage $contactMessage): View
|
||||
{
|
||||
return view('admin.contacts.show', compact('contactMessage'));
|
||||
}
|
||||
|
||||
public function destroy(ContactMessage $contactMessage)
|
||||
{
|
||||
$contactMessage->delete();
|
||||
|
||||
return redirect()->route('admin.contacts.index')->with('success', '留言已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Article;
|
||||
use App\Models\Category;
|
||||
use App\Models\ContactMessage;
|
||||
use App\Models\ForumThread;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$stats = [
|
||||
'products' => Product::count(),
|
||||
'articles' => Article::count(),
|
||||
'categories' => \App\Models\Category::count(),
|
||||
'glossary' => \App\Models\GlossaryTerm::count(),
|
||||
'threads' => ForumThread::count(),
|
||||
'users' => User::count(),
|
||||
'messages' => ContactMessage::count(),
|
||||
];
|
||||
|
||||
$recentMessages = ContactMessage::orderByDesc('created_at')->limit(5)->get();
|
||||
$recentThreads = ForumThread::with('user', 'board')->orderByDesc('created_at')->limit(5)->get();
|
||||
|
||||
return view('admin.dashboard', compact('stats', 'recentMessages', 'recentThreads'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 后台数据库管理:查看迁移状态(已执行 / 待执行),并可一键执行迁移升级。
|
||||
* 满足「数据库可以随时在后台升级」——代码上线后无需 SSH,点按钮即可 migrate。
|
||||
*/
|
||||
class DatabaseController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$connection = config('database.default');
|
||||
$dbName = config("database.connections.{$connection}.database");
|
||||
|
||||
$ran = [];
|
||||
if (Schema::hasTable('migrations')) {
|
||||
$ran = DB::table('migrations')->pluck('migration')->toArray();
|
||||
}
|
||||
|
||||
$files = glob(database_path('migrations/*.php'));
|
||||
$all = array_map(fn ($f) => basename($f, '.php'), $files);
|
||||
sort($all);
|
||||
|
||||
$done = array_values(array_intersect($all, $ran));
|
||||
$pending = array_values(array_diff($all, $ran));
|
||||
|
||||
return view('admin.database', compact('connection', 'dbName', 'pending', 'done'));
|
||||
}
|
||||
|
||||
public function upgrade(Request $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
Artisan::call('migrate', ['--force' => true]);
|
||||
$output = Artisan::output();
|
||||
|
||||
return redirect()->route('admin.database')
|
||||
->with('success', '数据库升级已执行完成。')
|
||||
->with('migrate_output', $output);
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()->route('admin.database')
|
||||
->with('error', '升级失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ForumBoard;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ForumBoardController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$boards = ForumBoard::orderBy('sort')->orderBy('name')->paginate(20);
|
||||
|
||||
return view('admin.forum.boards', compact('boards'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.forum.board-form', ['board' => null]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:60'],
|
||||
'slug' => ['required', 'string', 'max:80', 'unique:forum_boards,slug'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
ForumBoard::create($data);
|
||||
|
||||
return redirect()->route('admin.forum.boards.index')->with('success', '版块已创建');
|
||||
}
|
||||
|
||||
public function edit(ForumBoard $board): View
|
||||
{
|
||||
return view('admin.forum.board-form', compact('board'));
|
||||
}
|
||||
|
||||
public function update(Request $request, ForumBoard $board)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:60'],
|
||||
'slug' => ['required', 'string', 'max:80', 'unique:forum_boards,slug,' . $board->id],
|
||||
'description' => ['nullable', 'string'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
$board->update($data);
|
||||
|
||||
return redirect()->route('admin.forum.boards.index')->with('success', '版块已更新');
|
||||
}
|
||||
|
||||
public function destroy(ForumBoard $board)
|
||||
{
|
||||
$board->delete();
|
||||
|
||||
return redirect()->route('admin.forum.boards.index')->with('success', '版块已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ForumThread;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ForumThreadController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$threads = ForumThread::with('user', 'board')
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20);
|
||||
|
||||
return view('admin.forum.threads', compact('threads'));
|
||||
}
|
||||
|
||||
public function destroy(ForumThread $thread)
|
||||
{
|
||||
$thread->delete();
|
||||
|
||||
return redirect()->route('admin.forum.threads.index')->with('success', '帖子已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GlossaryTerm;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GlossaryController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$terms = GlossaryTerm::orderBy('term')->paginate(20);
|
||||
|
||||
return view('admin.glossary.index', compact('terms'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.glossary.form', ['term' => null]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'term' => ['required', 'string', 'max:80'],
|
||||
'slug' => ['required', 'string', 'max:90', 'unique:glossary_terms,slug'],
|
||||
'definition' => ['required', 'string'],
|
||||
'aliases' => ['nullable', 'string', 'max:255'],
|
||||
'category' => ['nullable', 'string', 'max:60'],
|
||||
]);
|
||||
|
||||
GlossaryTerm::create($data);
|
||||
|
||||
return redirect()->route('admin.glossary.index')->with('success', '术语已创建');
|
||||
}
|
||||
|
||||
public function edit(GlossaryTerm $glossaryTerm): View
|
||||
{
|
||||
return view('admin.glossary.form', ['term' => $glossaryTerm]);
|
||||
}
|
||||
|
||||
public function update(Request $request, GlossaryTerm $glossaryTerm)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'term' => ['required', 'string', 'max:80'],
|
||||
'slug' => ['required', 'string', 'max:90', 'unique:glossary_terms,slug,' . $glossaryTerm->id],
|
||||
'definition' => ['required', 'string'],
|
||||
'aliases' => ['nullable', 'string', 'max:255'],
|
||||
'category' => ['nullable', 'string', 'max:60'],
|
||||
]);
|
||||
|
||||
$glossaryTerm->update($data);
|
||||
|
||||
return redirect()->route('admin.glossary.index')->with('success', '术语已更新');
|
||||
}
|
||||
|
||||
public function destroy(GlossaryTerm $glossaryTerm)
|
||||
{
|
||||
$glossaryTerm->delete();
|
||||
|
||||
return redirect()->route('admin.glossary.index')->with('success', '术语已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\HeroSlide;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HeroSlideController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$slides = HeroSlide::where('location', 'home')->ordered()->get();
|
||||
$mode = Setting::get('hero_mode', 'carousel');
|
||||
|
||||
return view('admin.hero-slides.index', compact('slides', 'mode'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.hero-slides.form', ['slide' => null]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:120'],
|
||||
'content' => ['nullable', 'string'],
|
||||
'cta_text' => ['nullable', 'string', 'max:80'],
|
||||
'cta_url' => ['nullable', 'string', 'max:255'],
|
||||
'cta2_text' => ['nullable', 'string', 'max:80'],
|
||||
'cta2_url' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['boolean'],
|
||||
'sort_order' => ['integer'],
|
||||
]);
|
||||
|
||||
$data['location'] = 'home';
|
||||
$data['is_active'] = $request->boolean('is_active');
|
||||
$data['sort_order'] = $data['sort_order'] ?? 0;
|
||||
|
||||
HeroSlide::create($data);
|
||||
|
||||
return redirect()->route('admin.hero-slides.index')->with('success', '轮播项已添加');
|
||||
}
|
||||
|
||||
public function edit(HeroSlide $hero_slide): View
|
||||
{
|
||||
return view('admin.hero-slides.form', ['slide' => $hero_slide]);
|
||||
}
|
||||
|
||||
public function update(Request $request, HeroSlide $hero_slide): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:120'],
|
||||
'content' => ['nullable', 'string'],
|
||||
'cta_text' => ['nullable', 'string', 'max:80'],
|
||||
'cta_url' => ['nullable', 'string', 'max:255'],
|
||||
'cta2_text' => ['nullable', 'string', 'max:80'],
|
||||
'cta2_url' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['boolean'],
|
||||
'sort_order' => ['integer'],
|
||||
]);
|
||||
|
||||
$data['is_active'] = $request->boolean('is_active');
|
||||
$data['sort_order'] = $data['sort_order'] ?? $hero_slide->sort_order;
|
||||
|
||||
$hero_slide->update($data);
|
||||
|
||||
return redirect()->route('admin.hero-slides.index')->with('success', '轮播项已更新');
|
||||
}
|
||||
|
||||
public function destroy(HeroSlide $hero_slide): RedirectResponse
|
||||
{
|
||||
$hero_slide->delete();
|
||||
|
||||
return redirect()->route('admin.hero-slides.index')->with('success', '轮播项已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上移 / 下移排序。
|
||||
*/
|
||||
public function move(Request $request, HeroSlide $hero_slide): RedirectResponse
|
||||
{
|
||||
$direction = $request->input('direction');
|
||||
|
||||
if ($direction === 'up') {
|
||||
$sibling = HeroSlide::where('location', 'home')->ordered()
|
||||
->where('sort_order', '<', $hero_slide->sort_order)
|
||||
->orderByDesc('sort_order')
|
||||
->first();
|
||||
if ($sibling) {
|
||||
[$hero_slide->sort_order, $sibling->sort_order] = [$sibling->sort_order, $hero_slide->sort_order];
|
||||
$hero_slide->save();
|
||||
$sibling->save();
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$sibling = HeroSlide::where('location', 'home')->ordered()
|
||||
->where('sort_order', '>', $hero_slide->sort_order)
|
||||
->first();
|
||||
if ($sibling) {
|
||||
[$hero_slide->sort_order, $sibling->sort_order] = [$sibling->sort_order, $hero_slide->sort_order];
|
||||
$hero_slide->save();
|
||||
$sibling->save();
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('admin.hero-slides.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键启用 / 停用。
|
||||
*/
|
||||
public function toggle(HeroSlide $hero_slide): RedirectResponse
|
||||
{
|
||||
$hero_slide->update(['is_active' => ! $hero_slide->is_active]);
|
||||
|
||||
return redirect()->route('admin.hero-slides.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 Hero 显示模式:carousel(轮播)/ single(独立单图)。
|
||||
*/
|
||||
public function setMode(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'hero_mode' => ['required', 'in:carousel,single'],
|
||||
]);
|
||||
|
||||
Setting::set('hero_mode', $data['hero_mode'], 'appearance');
|
||||
|
||||
return redirect()->route('admin.hero-slides.index')->with('success', 'Hero 显示模式已更新');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NavigationMenu;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MenuController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$menus = NavigationMenu::ordered()->get();
|
||||
|
||||
return view('admin.menus.index', compact('menus'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.menus.form', ['menu' => null]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'label' => ['required', 'string', 'max:60'],
|
||||
'url' => ['required', 'string', 'max:255'],
|
||||
'is_visible' => ['boolean'],
|
||||
'sort_order' => ['integer'],
|
||||
]);
|
||||
|
||||
$data['is_visible'] = $request->boolean('is_visible');
|
||||
$data['sort_order'] = $data['sort_order'] ?? 0;
|
||||
|
||||
NavigationMenu::create($data);
|
||||
|
||||
return redirect()->route('admin.menus.index')->with('success', '菜单项已添加');
|
||||
}
|
||||
|
||||
public function edit(NavigationMenu $menu): View
|
||||
{
|
||||
return view('admin.menus.form', compact('menu'));
|
||||
}
|
||||
|
||||
public function update(Request $request, NavigationMenu $menu): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'label' => ['required', 'string', 'max:60'],
|
||||
'url' => ['required', 'string', 'max:255'],
|
||||
'is_visible' => ['boolean'],
|
||||
'sort_order' => ['integer'],
|
||||
]);
|
||||
|
||||
$data['is_visible'] = $request->boolean('is_visible');
|
||||
$data['sort_order'] = $data['sort_order'] ?? $menu->sort_order;
|
||||
|
||||
$menu->update($data);
|
||||
|
||||
return redirect()->route('admin.menus.index')->with('success', '菜单项已更新');
|
||||
}
|
||||
|
||||
public function destroy(NavigationMenu $menu): RedirectResponse
|
||||
{
|
||||
$menu->delete();
|
||||
|
||||
return redirect()->route('admin.menus.index')->with('success', '菜单项已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上移 / 下移排序。
|
||||
*/
|
||||
public function move(Request $request, NavigationMenu $menu): RedirectResponse
|
||||
{
|
||||
$direction = $request->input('direction');
|
||||
|
||||
if ($direction === 'up') {
|
||||
$sibling = NavigationMenu::ordered()
|
||||
->where('sort_order', '<', $menu->sort_order)
|
||||
->orderByDesc('sort_order')
|
||||
->first();
|
||||
if ($sibling) {
|
||||
[$menu->sort_order, $sibling->sort_order] = [$sibling->sort_order, $menu->sort_order];
|
||||
$menu->save();
|
||||
$sibling->save();
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$sibling = NavigationMenu::ordered()
|
||||
->where('sort_order', '>', $menu->sort_order)
|
||||
->first();
|
||||
if ($sibling) {
|
||||
[$menu->sort_order, $sibling->sort_order] = [$sibling->sort_order, $menu->sort_order];
|
||||
$menu->save();
|
||||
$sibling->save();
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('admin.menus.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键显示 / 隐藏。
|
||||
*/
|
||||
public function toggle(NavigationMenu $menu): RedirectResponse
|
||||
{
|
||||
$menu->update(['is_visible' => ! $menu->is_visible]);
|
||||
|
||||
return redirect()->route('admin.menus.index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Article;
|
||||
use App\Models\ForumReply;
|
||||
use App\Models\ForumThread;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 统一审核队列:文章投稿 / 论坛帖子 / 论坛回复 的待审内容。
|
||||
* 入口按权限展示对应 Tab(articles.moderate / forum.moderate)。
|
||||
*/
|
||||
class ModerationController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$tab = $request->query('tab', 'articles');
|
||||
|
||||
$canArticles = $user->canPerm('articles.moderate');
|
||||
$canForum = $user->canPerm('forum.moderate');
|
||||
|
||||
if (! $canArticles && ! $canForum) {
|
||||
abort(403, '无审核权限');
|
||||
}
|
||||
|
||||
if ($tab === 'threads' && ! $canForum) {
|
||||
$tab = $canArticles ? 'articles' : 'replies';
|
||||
}
|
||||
if ($tab === 'replies' && ! $canForum) {
|
||||
$tab = $canArticles ? 'articles' : 'threads';
|
||||
}
|
||||
|
||||
$articles = $canArticles
|
||||
? Article::where('status', 'pending')->with('user', 'category')->orderByDesc('created_at')->paginate(10, ['*'], 'ap')
|
||||
: null;
|
||||
|
||||
$threads = $canForum
|
||||
? ForumThread::where('status', 'pending')->with('user', 'board')->orderByDesc('created_at')->paginate(10, ['*'], 'tp')
|
||||
: null;
|
||||
|
||||
$replies = $canForum
|
||||
? ForumReply::where('status', 'pending')->with('user', 'thread')->orderByDesc('created_at')->paginate(10, ['*'], 'rp')
|
||||
: null;
|
||||
|
||||
return view('admin.moderation.index', compact('tab', 'articles', 'threads', 'replies', 'canArticles', 'canForum'));
|
||||
}
|
||||
|
||||
public function approveArticle(Article $article)
|
||||
{
|
||||
$article->update([
|
||||
'status' => 'approved',
|
||||
'published_at' => $article->published_at ?? now(),
|
||||
]);
|
||||
|
||||
return back()->with('success', "已通过文章《{$article->title}》");
|
||||
}
|
||||
|
||||
public function rejectArticle(Article $article)
|
||||
{
|
||||
$article->update(['status' => 'rejected']);
|
||||
|
||||
return back()->with('success', "已驳回文章《{$article->title}》");
|
||||
}
|
||||
|
||||
public function approveThread(ForumThread $thread)
|
||||
{
|
||||
$thread->update(['status' => ForumThread::STATUS_APPROVED]);
|
||||
|
||||
return back()->with('success', "已通过帖子《{$thread->title}》");
|
||||
}
|
||||
|
||||
public function rejectThread(ForumThread $thread)
|
||||
{
|
||||
$thread->update(['status' => ForumThread::STATUS_REJECTED]);
|
||||
|
||||
return back()->with('success', "已驳回帖子《{$thread->title}》");
|
||||
}
|
||||
|
||||
public function approveReply(ForumReply $reply)
|
||||
{
|
||||
$reply->update(['status' => ForumReply::STATUS_APPROVED]);
|
||||
|
||||
return back()->with('success', '已通过该回复');
|
||||
}
|
||||
|
||||
public function rejectReply(ForumReply $reply)
|
||||
{
|
||||
$reply->update(['status' => ForumReply::STATUS_REJECTED]);
|
||||
|
||||
return back()->with('success', '已驳回该回复');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\News;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NewsController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$news = News::orderByDesc('published_at')->paginate(20);
|
||||
|
||||
return view('admin.news.index', compact('news'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.news.form', ['news' => null]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:160'],
|
||||
'slug' => ['required', 'string', 'max:160', 'unique:news,slug'],
|
||||
'category' => ['required', 'in:industry,company,event'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'body' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', 'in:published,draft'],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
$data['published_at'] = $data['published_at'] ?? now();
|
||||
|
||||
News::create($data);
|
||||
|
||||
return redirect()->route('admin.news.index')->with('success', '新闻已创建');
|
||||
}
|
||||
|
||||
public function edit(News $news): View
|
||||
{
|
||||
return view('admin.news.form', compact('news'));
|
||||
}
|
||||
|
||||
public function update(Request $request, News $news)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:160'],
|
||||
'slug' => ['required', 'string', 'max:160', 'unique:news,slug,' . $news->id],
|
||||
'category' => ['required', 'in:industry,company,event'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'body' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', 'in:published,draft'],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
$news->update($data);
|
||||
|
||||
return redirect()->route('admin.news.index')->with('success', '新闻已更新');
|
||||
}
|
||||
|
||||
public function destroy(News $news)
|
||||
{
|
||||
$news->delete();
|
||||
|
||||
return redirect()->route('admin.news.index')->with('success', '新闻已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$products = Product::with('category')->orderBy('sort')->orderBy('name')->paginate(20);
|
||||
|
||||
return view('admin.products.index', compact('products'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$categories = Category::ofType('product')->orderBy('sort')->orderBy('name')->get();
|
||||
|
||||
return view('admin.products.form', ['product' => null, 'categories' => $categories]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'category_id' => ['nullable', 'exists:categories,id'],
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'slug' => ['required', 'string', 'max:120', 'unique:products,slug'],
|
||||
'model' => ['nullable', 'string', 'max:60'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'specs' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', 'in:published,draft'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
Product::create($data);
|
||||
|
||||
return redirect()->route('admin.products.index')->with('success', '产品已创建');
|
||||
}
|
||||
|
||||
public function edit(Product $product): View
|
||||
{
|
||||
$categories = Category::ofType('product')->orderBy('sort')->orderBy('name')->get();
|
||||
|
||||
return view('admin.products.form', compact('product', 'categories'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Product $product)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'category_id' => ['nullable', 'exists:categories,id'],
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'slug' => ['required', 'string', 'max:120', 'unique:products,slug,' . $product->id],
|
||||
'model' => ['nullable', 'string', 'max:60'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'specs' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', 'in:published,draft'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
$product->update($data);
|
||||
|
||||
return redirect()->route('admin.products.index')->with('success', '产品已更新');
|
||||
}
|
||||
|
||||
public function destroy(Product $product)
|
||||
{
|
||||
$product->delete();
|
||||
|
||||
return redirect()->route('admin.products.index')->with('success', '产品已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$settings = Setting::pluck('value', 'key')->toArray();
|
||||
|
||||
return view('admin.settings', compact('settings'));
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'site_name' => ['required', 'string', 'max:80'],
|
||||
'site_description' => ['nullable', 'string', 'max:255'],
|
||||
'seo_title_default' => ['nullable', 'string', 'max:160'],
|
||||
'seo_description_default' => ['nullable', 'string', 'max:255'],
|
||||
'icp_no' => ['nullable', 'string', 'max:60'],
|
||||
'security_filing_no' => ['nullable', 'string', 'max:60'],
|
||||
'logo_url' => ['nullable', 'string', 'max:255'],
|
||||
'favicon_url' => ['nullable', 'string', 'max:255'],
|
||||
'contact_email' => ['nullable', 'email', 'max:120'],
|
||||
'theme' => ['nullable', 'string', 'in:azure,slate,indigo,teal,amber'],
|
||||
'footer_content' => ['nullable', 'string', 'max:20000'],
|
||||
]);
|
||||
|
||||
$seoKeys = ['site_description', 'seo_title_default', 'seo_description_default'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$group = in_array($key, $seoKeys, true) ? 'seo' : 'general';
|
||||
Setting::set($key, $value ?: null, $group);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.settings')->with('success', '站点设置已保存');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Solution;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SolutionController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$solutions = Solution::orderBy('sort')->orderBy('name')->paginate(20);
|
||||
|
||||
return view('admin.solutions.index', compact('solutions'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.solutions.form', ['solution' => null]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'slug' => ['required', 'string', 'max:120', 'unique:solutions,slug'],
|
||||
'power_segment' => ['nullable', 'in:lt30,30-65,65-100,gt100'],
|
||||
'application' => ['nullable', 'in:charger,powerbank,car,dock'],
|
||||
'chip_platform' => ['nullable', 'in:protocol,soc,rectifier'],
|
||||
'power' => ['nullable', 'string', 'max:40'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'advantages' => ['nullable', 'string'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', 'in:published,draft'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
Solution::create($data);
|
||||
|
||||
return redirect()->route('admin.solutions.index')->with('success', '方案已创建');
|
||||
}
|
||||
|
||||
public function edit(Solution $solution): View
|
||||
{
|
||||
return view('admin.solutions.form', compact('solution'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Solution $solution)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'slug' => ['required', 'string', 'max:120', 'unique:solutions,slug,' . $solution->id],
|
||||
'power_segment' => ['nullable', 'in:lt30,30-65,65-100,gt100'],
|
||||
'application' => ['nullable', 'in:charger,powerbank,car,dock'],
|
||||
'chip_platform' => ['nullable', 'in:protocol,soc,rectifier'],
|
||||
'power' => ['nullable', 'string', 'max:40'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'advantages' => ['nullable', 'string'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['required', 'in:published,draft'],
|
||||
'sort' => ['integer'],
|
||||
]);
|
||||
|
||||
$solution->update($data);
|
||||
|
||||
return redirect()->route('admin.solutions.index')->with('success', '方案已更新');
|
||||
}
|
||||
|
||||
public function destroy(Solution $solution)
|
||||
{
|
||||
$solution->delete();
|
||||
|
||||
return redirect()->route('admin.solutions.index')->with('success', '方案已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$users = User::query()
|
||||
->when($q, fn ($query) => $query->where(function ($sub) use ($q) {
|
||||
$sub->where('name', 'like', "%{$q}%")->orWhere('email', 'like', "%{$q}%");
|
||||
}))
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.users.index', compact('users', 'q'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建用户表单(角色 + 权限分配)。
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$roles = config('permissions.roles');
|
||||
$groups = config('permissions.groups');
|
||||
$permissions = config('permissions.permissions');
|
||||
|
||||
return view('admin.users.create', compact('roles', 'groups', 'permissions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建用户,受层级约束:不能创建权限高于或等于自身角色的用户。
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$current = $request->user();
|
||||
|
||||
$allPermKeys = implode(',', array_keys(config('permissions.permissions')));
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:60'],
|
||||
'email' => ['required', 'email', 'unique:users,email'],
|
||||
'password' => ['required', 'string', 'min:8'],
|
||||
'role' => ['required', 'in:super_admin,admin,forum_admin,staff,user'],
|
||||
'permissions' => ['nullable', 'array'],
|
||||
'permissions.*' => ['string', "in:{$allPermKeys}"],
|
||||
]);
|
||||
|
||||
// 层级防护:不能创建高于或等于自身权限的角色
|
||||
if (! $current->isSuperAdmin() && $current->roleLevel() <= $this->roleLevelOf($data['role'])) {
|
||||
return back()->withErrors(['role' => '您只能创建权限低于自身的用户'])->withInput();
|
||||
}
|
||||
if ($data['role'] === 'super_admin' && ! $current->isSuperAdmin()) {
|
||||
return back()->withErrors(['role' => '只有超级管理员可以创建超级管理员'])->withInput();
|
||||
}
|
||||
|
||||
$permissions = $data['role'] === 'staff'
|
||||
? array_values(array_unique($data['permissions'] ?? []))
|
||||
: null;
|
||||
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'role' => $data['role'],
|
||||
'is_admin' => $data['role'] !== 'user',
|
||||
'permissions' => $permissions,
|
||||
'points' => 0,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', "已创建用户 {$user->name}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户角色与权限(内部员工可勾选细粒度权限)。
|
||||
*/
|
||||
public function edit(User $user)
|
||||
{
|
||||
$roles = config('permissions.roles');
|
||||
$groups = config('permissions.groups');
|
||||
$permissions = config('permissions.permissions');
|
||||
$userPerms = is_array($user->permissions) ? $user->permissions : [];
|
||||
|
||||
$canManage = $this->canManage($user);
|
||||
|
||||
return view('admin.users.edit', compact('user', 'roles', 'groups', 'permissions', 'userPerms', 'canManage'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户角色与权限,受层级约束。
|
||||
*/
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$current = $request->user();
|
||||
|
||||
// 防锁死:禁止改动自己
|
||||
if ($user->id === $current->id) {
|
||||
return back()->withErrors(['role' => '不能修改当前登录账号的角色/权限']);
|
||||
}
|
||||
|
||||
// 层级防护:非超管不能触碰同级或更高权限用户
|
||||
if (! $current->isSuperAdmin() && $current->roleLevel() <= $user->roleLevel()) {
|
||||
return back()->withErrors(['role' => '您只能修改权限低于自身的用户']);
|
||||
}
|
||||
if ($user->isSuperAdmin() && ! $current->isSuperAdmin()) {
|
||||
return back()->withErrors(['role' => '只有超级管理员可以调整超级管理员']);
|
||||
}
|
||||
|
||||
$allPermKeys = implode(',', array_keys(config('permissions.permissions')));
|
||||
$data = $request->validate([
|
||||
'role' => ['required', 'in:super_admin,admin,forum_admin,staff,user'],
|
||||
'permissions' => ['nullable', 'array'],
|
||||
'permissions.*' => ['string', "in:{$allPermKeys}"],
|
||||
]);
|
||||
|
||||
// 越权防护:不能分配高于或等于自身权限的角色
|
||||
if ($data['role'] === 'super_admin' && ! $current->isSuperAdmin()) {
|
||||
return back()->withErrors(['role' => '只有超级管理员可以分配超级管理员角色']);
|
||||
}
|
||||
if (! $current->isSuperAdmin() && $current->roleLevel() <= $this->roleLevelOf($data['role'])) {
|
||||
return back()->withErrors(['role' => '您只能分配权限低于自身的角色']);
|
||||
}
|
||||
|
||||
$permissions = $data['role'] === 'staff'
|
||||
? array_values(array_unique($data['permissions'] ?? []))
|
||||
: null;
|
||||
|
||||
$user->update([
|
||||
'role' => $data['role'],
|
||||
'is_admin' => $data['role'] !== 'user',
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', "已更新 {$user->name} 的角色与权限");
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定用户密码(管理员代操作):可指定新密码,留空则生成随机密码并回显。
|
||||
* 受层级约束,且禁止重置当前登录账号(请用「修改密码」)。
|
||||
*/
|
||||
public function resetPassword(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$current = $request->user();
|
||||
|
||||
if ($user->id === $current->id) {
|
||||
return back()->withErrors(['password' => '不能重置当前登录账号的密码,请到「修改密码」操作']);
|
||||
}
|
||||
if (! $current->isSuperAdmin() && $current->roleLevel() <= $user->roleLevel()) {
|
||||
return back()->withErrors(['password' => '您只能重置权限低于自身的用户密码']);
|
||||
}
|
||||
if ($user->isSuperAdmin() && ! $current->isSuperAdmin()) {
|
||||
return back()->withErrors(['password' => '只有超级管理员可以重置超级管理员密码']);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||
]);
|
||||
|
||||
if (! empty($data['password'])) {
|
||||
$new = $data['password'];
|
||||
$msg = '密码已重置';
|
||||
} else {
|
||||
$new = $this->generatePassword(12);
|
||||
$msg = '密码已重置为随机密码:' . $new . '(请妥善告知用户,并建议其尽快修改)';
|
||||
}
|
||||
|
||||
$user->update(['password' => Hash::make($new)]);
|
||||
|
||||
return redirect()->route('admin.users.edit', $user)->with('success', $msg);
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$current = request()->user();
|
||||
|
||||
if ($user->id === $current->id) {
|
||||
return back()->withErrors(['name' => '不能删除当前登录账号']);
|
||||
}
|
||||
if (! $current->isSuperAdmin() && $current->roleLevel() <= $user->roleLevel()) {
|
||||
return back()->withErrors(['name' => '您只能删除权限低于自身的用户']);
|
||||
}
|
||||
if ($user->isSuperAdmin() && ! $current->isSuperAdmin()) {
|
||||
return back()->withErrors(['name' => '只有超级管理员可以删除超级管理员']);
|
||||
}
|
||||
|
||||
$user->delete();
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', '用户已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户是否可管理目标用户(用于视图显隐操作按钮)。
|
||||
*/
|
||||
protected function canManage(User $user): bool
|
||||
{
|
||||
$current = request()->user();
|
||||
if ($current->isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
return $current->roleLevel() > $user->roleLevel();
|
||||
}
|
||||
|
||||
protected function roleLevelOf(string $role): int
|
||||
{
|
||||
return (int) config("permissions.roles.{$role}.level", 1);
|
||||
}
|
||||
|
||||
protected function generatePassword(int $len): string
|
||||
{
|
||||
$pool = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
$out = '';
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$out .= $pool[random_int(0, strlen($pool) - 1)];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ArticleController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$categoryId = $request->query('category');
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$categories = Category::ofType('article')->orderBy('sort')->orderBy('name')->get();
|
||||
|
||||
$articles = Article::published()
|
||||
->when($categoryId, fn ($query) => $query->where('category_id', $categoryId))
|
||||
->when($q, function ($query) use ($q) {
|
||||
$query->where(function ($sub) use ($q) {
|
||||
$sub->where('title', 'like', "%{$q}%")
|
||||
->orWhere('summary', 'like', "%{$q}%");
|
||||
});
|
||||
})
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('articles.index', compact('articles', 'categories', 'q', 'categoryId'));
|
||||
}
|
||||
|
||||
public function show(Article $article): View
|
||||
{
|
||||
$user = request()->user();
|
||||
|
||||
// 公开:已审且已发布;作者本人或审核员:可看自己投稿(含待审/驳回)
|
||||
$canView = $article->status === 'approved' && $article->published_at !== null;
|
||||
if (! $canView && $user) {
|
||||
$canView = $article->user_id === $user->id || $user->canPerm('articles.moderate');
|
||||
}
|
||||
if (! $canView) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('articles.show', compact('article'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户投稿表单(需登录)。
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$categories = Category::ofType('article')->orderBy('sort')->orderBy('name')->get();
|
||||
|
||||
return view('articles.submit', compact('categories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交用户投稿:写入 articles(user_id + status=pending),审核前仅作者可见。
|
||||
*/
|
||||
public function storeSubmission(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'category_id' => ['nullable', 'exists:categories,id'],
|
||||
'title' => ['required', 'string', 'max:160'],
|
||||
'summary' => ['nullable', 'string', 'max:255'],
|
||||
'body' => ['required', 'string'],
|
||||
'template' => ['required', 'in:standard,tech,feature'],
|
||||
'cover' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$data['body'] = \Purifier::clean($data['body'], 'article');
|
||||
|
||||
$base = Str::slug(Str::ascii($data['title'])) ?: ('article-' . date('YmdHis'));
|
||||
$slug = $base;
|
||||
$i = 1;
|
||||
// withTrashed:软删除行仍占用 slug 唯一索引,须一并检查避免 Duplicate entry
|
||||
while (Article::withTrashed()->where('slug', $slug)->exists()) {
|
||||
$slug = $base . '-' . $i++;
|
||||
}
|
||||
|
||||
Article::create([
|
||||
'category_id' => $data['category_id'] ?? null,
|
||||
'user_id' => Auth::id(),
|
||||
'title' => $data['title'],
|
||||
'slug' => $slug,
|
||||
'summary' => $data['summary'] ?? null,
|
||||
'body' => $data['body'],
|
||||
'template' => $data['template'],
|
||||
'cover' => $data['cover'] ?? null,
|
||||
'author' => Auth::user()->name,
|
||||
'status' => 'pending',
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
return redirect()->route('articles.mine')
|
||||
->with('success', '投稿已提交,管理员审核通过后将公开展示');
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的投稿:当前用户自己的全部文章(含待审/驳回)。
|
||||
*/
|
||||
public function mine(): View
|
||||
{
|
||||
$articles = Article::where('user_id', Auth::id())
|
||||
->orderByDesc('created_at')
|
||||
->paginate(10);
|
||||
|
||||
return view('articles.mine', compact('articles'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 管理员独立登录:与普通用户登录(/login)分离到不同页面。
|
||||
* 仅允许具备后台角色(isAdmin)的账户登录,其余账户直接拒绝。
|
||||
* 含图形验证码 + 10 分钟 5 次错误锁定的人机/防暴破管控。
|
||||
*/
|
||||
class AdminLoginController extends Controller
|
||||
{
|
||||
protected const MAX_ATTEMPTS = 5;
|
||||
protected const DECAY_SECONDS = 600;
|
||||
|
||||
public function show(): View
|
||||
{
|
||||
return view('auth.admin-login');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$key = $this->throttleKey($request);
|
||||
|
||||
if (RateLimiter::tooManyAttempts($key, self::MAX_ATTEMPTS)) {
|
||||
$seconds = RateLimiter::availableIn($key);
|
||||
$request->session()->forget('captcha');
|
||||
throw ValidationException::withMessages([
|
||||
'email' => '登录尝试过于频繁,出于安全考虑已临时锁定。请在约 '.ceil($seconds / 60).' 分钟('.ceil($seconds).' 秒)后重试。',
|
||||
]);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
'captcha' => ['required', 'string', function ($attribute, $value, $fail) use ($request) {
|
||||
if (strtolower(trim((string) $value)) !== strtolower((string) $request->session()->get('captcha', ''))) {
|
||||
$fail('图形验证码不正确,请重新输入。');
|
||||
}
|
||||
}],
|
||||
]);
|
||||
|
||||
if (! Auth::attempt(['email' => $data['email'], 'password' => $data['password']], $request->boolean('remember'))) {
|
||||
RateLimiter::hit($key, self::DECAY_SECONDS);
|
||||
$attempts = RateLimiter::attempts($key);
|
||||
$left = max(0, self::MAX_ATTEMPTS - $attempts);
|
||||
$request->session()->forget('captcha');
|
||||
|
||||
$msg = '账号或密码不正确。';
|
||||
if ($left > 0) {
|
||||
$msg .= "(已错误 {$attempts} 次,再错 {$left} 次将锁定 10 分钟)";
|
||||
} else {
|
||||
$msg .= '(错误次数过多,已锁定 10 分钟)';
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => $msg,
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user->isAdmin()) {
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
$request->session()->forget('captcha');
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => '该账户没有后台管理权限。',
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
RateLimiter::clear($key);
|
||||
$request->session()->forget('captcha');
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended('/admin');
|
||||
}
|
||||
|
||||
protected function throttleKey(Request $request): string
|
||||
{
|
||||
return 'admin-login:'.strtolower($request->input('email', '')).':'.$request->ip();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
/**
|
||||
* 图形验证码(轻量、零依赖,基于 GD 绘制)。
|
||||
* - GET /captcha 生成一张 4 位字符的图片,并把答案写入 session('captcha')。
|
||||
* - 登录接口校验用户输入与 session 中的值(大小写不敏感),校验后清空,防止复用。
|
||||
* - 仅用于「人/机」区分,配合登录限流构成基础安全管控。
|
||||
*/
|
||||
class CaptchaController extends Controller
|
||||
{
|
||||
public function show(Request $request)
|
||||
{
|
||||
$code = $this->generateCode(4);
|
||||
Session::put('captcha', $code);
|
||||
|
||||
$width = 120;
|
||||
$height = 44;
|
||||
$img = imagecreatetruecolor($width, $height);
|
||||
|
||||
// 背景
|
||||
$bg = imagecolorallocate($img, 244, 247, 250);
|
||||
imagefilledrectangle($img, 0, 0, $width, $height, $bg);
|
||||
|
||||
// 干扰线
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$c = imagecolorallocate($img, rand(160, 210), rand(160, 210), rand(160, 210));
|
||||
imageline($img, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $c);
|
||||
}
|
||||
|
||||
// 字符(随机色 + 轻微纵向抖动)
|
||||
$palette = [[74, 138, 244], [110, 140, 176], [43, 182, 164], [123, 126, 240], [200, 120, 60]];
|
||||
$len = strlen($code);
|
||||
$step = (int) ($width / ($len + 1));
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$col = $palette[array_rand($palette)];
|
||||
$c = imagecolorallocate($img, $col[0], $col[1], $col[2]);
|
||||
$x = $step * ($i + 1) - 10 + rand(-3, 3);
|
||||
$y = rand(8, 18);
|
||||
imagestring($img, 5, $x, $y, $code[$i], $c);
|
||||
}
|
||||
|
||||
// 噪点
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$c = imagecolorallocate($img, rand(180, 225), rand(180, 225), rand(180, 225));
|
||||
imagesetpixel($img, rand(0, $width), rand(0, $height), $c);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
imagepng($img);
|
||||
$data = ob_get_clean();
|
||||
imagedestroy($img);
|
||||
|
||||
return response($data, 200)
|
||||
->header('Content-Type', 'image/png')
|
||||
->header('Cache-Control', 'no-store, no-cache, must-revalidate, private')
|
||||
->header('Pragma', 'no-cache');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码字符(去除易混淆的 0/O/1/I/L)。
|
||||
*/
|
||||
protected function generateCode(int $len): string
|
||||
{
|
||||
$pool = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
$out = '';
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$out .= $pool[random_int(0, strlen($pool) - 1)];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
// 限流参数:10 分钟内最多 5 次错误,超出后锁定至窗口结束(≈10 分钟)
|
||||
protected const MAX_ATTEMPTS = 5;
|
||||
protected const DECAY_SECONDS = 600;
|
||||
|
||||
public function show(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$key = $this->throttleKey($request);
|
||||
|
||||
// 锁定检查:10 分钟内错误满 5 次即锁定,提示等待时长
|
||||
if (RateLimiter::tooManyAttempts($key, self::MAX_ATTEMPTS)) {
|
||||
$seconds = RateLimiter::availableIn($key);
|
||||
$request->session()->forget('captcha');
|
||||
throw ValidationException::withMessages([
|
||||
'email' => '登录尝试过于频繁,出于安全考虑已临时锁定。请在约 '.ceil($seconds / 60).' 分钟('.ceil($seconds).' 秒)后重试。',
|
||||
]);
|
||||
}
|
||||
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
'captcha' => ['required', 'string', function ($attribute, $value, $fail) use ($request) {
|
||||
if (strtolower(trim((string) $value)) !== strtolower((string) $request->session()->get('captcha', ''))) {
|
||||
$fail('图形验证码不正确,请重新输入。');
|
||||
}
|
||||
}],
|
||||
]);
|
||||
|
||||
if (Auth::attempt(['email' => $credentials['email'], 'password' => $credentials['password']], $request->boolean('remember'))) {
|
||||
RateLimiter::clear($key);
|
||||
$request->session()->forget('captcha');
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
// 失败:记录一次错误(10 分钟滚动窗口),并给出剩余次数提醒
|
||||
RateLimiter::hit($key, self::DECAY_SECONDS);
|
||||
$attempts = RateLimiter::attempts($key);
|
||||
$left = max(0, self::MAX_ATTEMPTS - $attempts);
|
||||
$request->session()->forget('captcha');
|
||||
|
||||
$msg = '账号或密码不正确。';
|
||||
if ($left > 0) {
|
||||
$msg .= "(已错误 {$attempts} 次,再错 {$left} 次将锁定 10 分钟)";
|
||||
} else {
|
||||
$msg .= '(错误次数过多,已锁定 10 分钟)';
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => $msg,
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
protected function throttleKey(Request $request): string
|
||||
{
|
||||
return 'login:'.strtolower($request->input('email', '')).':'.$request->ip();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录。定义为控制器方法而非闭包,
|
||||
* 以保证生产环境 `php artisan route:cache` 可用。
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
public function show(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:40'],
|
||||
'email' => ['required', 'email', 'max:120', 'unique:users,email'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
'real_name' => ['nullable', 'string', 'max:40'],
|
||||
'phone' => ['nullable', 'string', 'max:30'],
|
||||
'company' => ['nullable', 'string', 'max:120'],
|
||||
'industry' => ['nullable', 'string', 'max:60'],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
'real_name' => $data['real_name'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'company' => $data['company'] ?? null,
|
||||
'industry' => $data['industry'] ?? null,
|
||||
]);
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$user->awardPoints(20);
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('auth.settings');
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'confirmed', Password::min(8)],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
|
||||
return redirect()->route('auth.settings')->with('success', '密码已修改');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ContactMessage;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ContactController extends Controller
|
||||
{
|
||||
public function show(): View
|
||||
{
|
||||
return view('contact');
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse|RedirectResponse
|
||||
{
|
||||
$key = 'contact:'.$request->ip();
|
||||
|
||||
if (RateLimiter::tooManyAttempts($key, 5, 60)) {
|
||||
$seconds = RateLimiter::availableIn($key);
|
||||
$msg = "提交过于频繁,请 {$seconds} 秒后再试。";
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['ok' => false, 'message' => $msg], 429);
|
||||
}
|
||||
|
||||
return back()->withErrors(['message' => $msg])->withInput();
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:40'],
|
||||
'email' => ['required', 'email', 'max:120'],
|
||||
'phone' => ['nullable', 'string', 'max:20', 'regex:/^[0-9+\-()\s]+$/'],
|
||||
'company' => ['nullable', 'string', 'max:120'],
|
||||
'subject' => ['required', 'string', 'max:80'],
|
||||
'message' => ['required', 'string', 'max:1000'],
|
||||
]);
|
||||
|
||||
ContactMessage::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'company' => $data['company'] ?? null,
|
||||
'subject' => $data['subject'],
|
||||
'message' => $data['message'],
|
||||
'ip' => $request->ip(),
|
||||
]);
|
||||
|
||||
RateLimiter::hit($key, 60);
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['ok' => true, 'message' => '提交成功,我们会尽快与您联系。']);
|
||||
}
|
||||
|
||||
return back()->with('success', '提交成功,我们会尽快与您联系。');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ForumBoard;
|
||||
use App\Models\ForumReply;
|
||||
use App\Models\ForumThread;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ForumController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$boards = ForumBoard::orderBy('sort')->orderBy('name')
|
||||
->withCount(['threads' => fn ($q) => $q->where('status', ForumThread::STATUS_APPROVED)])
|
||||
->get();
|
||||
|
||||
return view('forum.index', compact('boards'));
|
||||
}
|
||||
|
||||
public function board(ForumBoard $board): View
|
||||
{
|
||||
$threads = $board->threads()
|
||||
->visible(Auth::user())
|
||||
->with('user')
|
||||
->orderByDesc('is_pinned')
|
||||
->orderByDesc('last_reply_at')
|
||||
->orderByDesc('created_at')
|
||||
->paginate(15);
|
||||
|
||||
return view('forum.board', compact('board', 'threads'));
|
||||
}
|
||||
|
||||
public function thread(ForumThread $thread): View
|
||||
{
|
||||
if ($thread->trashed()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$isMod = $user && $user->canPerm('forum.moderate');
|
||||
$isOwner = $user && $thread->user_id === $user->id;
|
||||
|
||||
// 待审且非作者、非审核员:不可见
|
||||
if ($thread->status !== ForumThread::STATUS_APPROVED && ! $isMod && ! $isOwner) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$thread->increment('views');
|
||||
|
||||
$replies = $thread->replies()
|
||||
->visible($user)
|
||||
->with('user')
|
||||
->orderBy('created_at')
|
||||
->paginate(20);
|
||||
|
||||
return view('forum.thread', compact('thread', 'replies'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'board_id' => ['required', 'exists:forum_boards,id'],
|
||||
'title' => ['required', 'string', 'max:120'],
|
||||
'body' => ['required', 'string', 'max:8000'],
|
||||
]);
|
||||
|
||||
$thread = ForumThread::create([
|
||||
'board_id' => $data['board_id'],
|
||||
'user_id' => Auth::id(),
|
||||
'title' => $data['title'],
|
||||
'body' => $data['body'],
|
||||
'status' => ForumThread::STATUS_PENDING,
|
||||
'last_reply_at' => now(),
|
||||
]);
|
||||
|
||||
$request->user()->awardPoints(10);
|
||||
|
||||
return redirect()->route('forum.thread', $thread)
|
||||
->with('success', '帖子已提交,管理员审核通过后将公开展示');
|
||||
}
|
||||
|
||||
public function reply(Request $request, ForumThread $thread)
|
||||
{
|
||||
if ($thread->is_locked) {
|
||||
return back()->withErrors(['body' => '该帖子已锁定,无法回复。']);
|
||||
}
|
||||
|
||||
// 待审帖子的回复入口对普通用户不可见(thread() 已拦截),此处仅作兜底
|
||||
if ($thread->status !== ForumThread::STATUS_APPROVED && ! Auth::user()?->canPerm('forum.moderate')) {
|
||||
return back()->withErrors(['body' => '该帖子尚未通过审核,暂不可回复。']);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'body' => ['required', 'string', 'max:8000'],
|
||||
]);
|
||||
|
||||
ForumReply::create([
|
||||
'thread_id' => $thread->id,
|
||||
'user_id' => Auth::id(),
|
||||
'body' => $data['body'],
|
||||
'status' => ForumReply::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
$thread->update(['last_reply_at' => now()]);
|
||||
|
||||
$request->user()->awardPoints(5);
|
||||
|
||||
return back()->with('success', '回复已提交,审核通过后将公开展示');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\GlossaryTerm;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GlossaryController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$terms = GlossaryTerm::ordered()
|
||||
->when($q, function ($query) use ($q) {
|
||||
$query->where(function ($sub) use ($q) {
|
||||
$sub->where('term', 'like', "%{$q}%")
|
||||
->orWhere('definition', 'like', "%{$q}%")
|
||||
->orWhere('aliases', 'like', "%{$q}%");
|
||||
});
|
||||
})
|
||||
->paginate(20)
|
||||
->withQueryString();
|
||||
|
||||
return view('glossary.index', compact('terms', 'q'));
|
||||
}
|
||||
|
||||
public function show(GlossaryTerm $glossaryTerm): View
|
||||
{
|
||||
return view('glossary.show', compact('glossaryTerm'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class HealthController extends Controller
|
||||
{
|
||||
/**
|
||||
* 健康检查端点,供部署脚本与监控探针使用。
|
||||
* 定义为控制器而非闭包,保证 route:cache 无隐患。
|
||||
*/
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
$dbOk = true;
|
||||
try {
|
||||
DB::connection()->getPdo();
|
||||
} catch (\Throwable) {
|
||||
$dbOk = false;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => $dbOk ? 'ok' : 'degraded',
|
||||
'database' => $dbOk ? 'up' : 'down',
|
||||
'time' => now()->toIso8601String(),
|
||||
], $dbOk ? 200 : 503);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\News;
|
||||
use App\Models\Product;
|
||||
use App\Models\Solution;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$products = Product::published()->take(3)->get();
|
||||
|
||||
$articles = Article::published()->take(3)->get();
|
||||
|
||||
$solutions = Solution::published()->orderBy('sort')->take(4)->get();
|
||||
|
||||
$news = News::published()->take(3)->get();
|
||||
|
||||
// 首页 Hero 轮播(后台「首页轮播」管理;mode=single 时为独立单图)
|
||||
$heroMode = \App\Models\Setting::get('hero_mode', 'carousel');
|
||||
$heroSlides = \App\Models\HeroSlide::location('home')->active()->ordered()->get();
|
||||
|
||||
return view('home', compact('products', 'articles', 'solutions', 'news', 'heroSlides', 'heroMode'));
|
||||
}
|
||||
|
||||
public function about(): View
|
||||
{
|
||||
return view('about');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\News;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NewsController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$category = $request->query('category');
|
||||
|
||||
$news = News::published()
|
||||
->when($category, fn ($q) => $q->where('category', $category))
|
||||
->paginate(9)
|
||||
->withQueryString();
|
||||
|
||||
return view('news.index', compact('news', 'category'));
|
||||
}
|
||||
|
||||
public function show(News $news): View
|
||||
{
|
||||
if ($news->status !== 'published' || $news->published_at === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('news.show', compact('news'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$categoryId = $request->query('category');
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$categories = Category::ofType('product')->orderBy('sort')->orderBy('name')->get();
|
||||
|
||||
$products = Product::published()
|
||||
->when($categoryId, fn ($query) => $query->where('category_id', $categoryId))
|
||||
->when($q, function ($query) use ($q) {
|
||||
$query->where(function ($sub) use ($q) {
|
||||
$sub->where('name', 'like', "%{$q}%")
|
||||
->orWhere('model', 'like', "%{$q}%")
|
||||
->orWhere('summary', 'like', "%{$q}%");
|
||||
});
|
||||
})
|
||||
->paginate(12)
|
||||
->withQueryString();
|
||||
|
||||
return view('products.index', compact('products', 'categories', 'q', 'categoryId'));
|
||||
}
|
||||
|
||||
public function show(Product $product): View
|
||||
{
|
||||
if ($product->status !== 'published') {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('products.show', compact('product'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\ForumThread;
|
||||
use App\Models\GlossaryTerm;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
/**
|
||||
* 全站搜索:跨 文章 / 产品 / 术语库 / 技术论坛 检索。
|
||||
* 各数据源遵循其前台可见性作用域(游客仅看已审内容)。
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
$user = $request->user();
|
||||
|
||||
$results = [
|
||||
'articles' => collect(),
|
||||
'products' => collect(),
|
||||
'glossary' => collect(),
|
||||
'threads' => collect(),
|
||||
];
|
||||
$total = 0;
|
||||
|
||||
if ($q !== '') {
|
||||
$like = '%' . $q . '%';
|
||||
|
||||
$results['articles'] = Article::visiblePublic()
|
||||
->where(function ($query) use ($like) {
|
||||
$query->where('title', 'like', $like)
|
||||
->orWhere('summary', 'like', $like)
|
||||
->orWhere('body', 'like', $like);
|
||||
})
|
||||
->orderByDesc('published_at')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
$results['products'] = Product::published()
|
||||
->where(function ($query) use ($like) {
|
||||
$query->where('name', 'like', $like)
|
||||
->orWhere('summary', 'like', $like)
|
||||
->orWhere('description', 'like', $like)
|
||||
->orWhere('model', 'like', $like);
|
||||
})
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
$results['glossary'] = GlossaryTerm::query()
|
||||
->where(function ($query) use ($like) {
|
||||
$query->where('term', 'like', $like)
|
||||
->orWhere('definition', 'like', $like)
|
||||
->orWhere('aliases', 'like', $like);
|
||||
})
|
||||
->orderBy('term')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
$results['threads'] = ForumThread::visible($user)
|
||||
->where(function ($query) use ($like) {
|
||||
$query->where('title', 'like', $like)
|
||||
->orWhere('body', 'like', $like);
|
||||
})
|
||||
->orderByDesc('last_reply_at')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
$total = $results['articles']->count()
|
||||
+ $results['products']->count()
|
||||
+ $results['glossary']->count()
|
||||
+ $results['threads']->count();
|
||||
}
|
||||
|
||||
return view('search.index', compact('q', 'results', 'total'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\GlossaryTerm;
|
||||
use App\Models\Product;
|
||||
use App\Models\Solution;
|
||||
use App\Models\News;
|
||||
|
||||
class SitemapController extends Controller
|
||||
{
|
||||
/**
|
||||
* 机读站点地图(SEO:供百度 / Google 等搜索引擎提交)。
|
||||
* 访问 /sitemap.xml 获取。
|
||||
*/
|
||||
public function xml()
|
||||
{
|
||||
$urls = [];
|
||||
|
||||
// ── 核心静态页面 ──────────────────────────────
|
||||
$core = [
|
||||
'/' => ['freq' => 'daily', 'pri' => '1.0'],
|
||||
'/about' => ['freq' => 'monthly', 'pri' => '0.8'],
|
||||
'/products' => ['freq' => 'weekly', 'pri' => '0.8'],
|
||||
'/articles' => ['freq' => 'weekly', 'pri' => '0.8'],
|
||||
'/solutions' => ['freq' => 'weekly', 'pri' => '0.8'],
|
||||
'/news' => ['freq' => 'weekly', 'pri' => '0.8'],
|
||||
'/glossary' => ['freq' => 'weekly', 'pri' => '0.8'],
|
||||
'/forum' => ['freq' => 'daily', 'pri' => '0.7'],
|
||||
'/contact' => ['freq' => 'yearly', 'pri' => '0.6'],
|
||||
'/sitemap' => ['freq' => 'monthly', 'pri' => '0.3'],
|
||||
];
|
||||
|
||||
foreach ($core as $path => $meta) {
|
||||
$urls[] = [
|
||||
'loc' => url($path),
|
||||
'freq' => $meta['freq'],
|
||||
'pri' => $meta['pri'],
|
||||
];
|
||||
}
|
||||
|
||||
// ── 动态内容:产品 ────────────────────────────
|
||||
foreach (Product::published()->get() as $p) {
|
||||
$urls[] = [
|
||||
'loc' => route('products.show', $p->slug),
|
||||
'last' => $p->updated_at?->toW3CString(),
|
||||
'freq' => 'weekly',
|
||||
'pri' => '0.7',
|
||||
];
|
||||
}
|
||||
|
||||
// ── 动态内容:技术文章 ────────────────────────
|
||||
foreach (Article::published()->get() as $a) {
|
||||
$urls[] = [
|
||||
'loc' => route('articles.show', $a->slug),
|
||||
'last' => $a->published_at?->toW3CString(),
|
||||
'freq' => 'monthly',
|
||||
'pri' => '0.6',
|
||||
];
|
||||
}
|
||||
|
||||
// ── 动态内容:术语库 ──────────────────────────
|
||||
foreach (GlossaryTerm::ordered()->get() as $g) {
|
||||
$urls[] = [
|
||||
'loc' => route('glossary.show', $g->slug),
|
||||
'freq' => 'monthly',
|
||||
'pri' => '0.4',
|
||||
];
|
||||
}
|
||||
|
||||
// ── 动态内容:解决方案 ────────────────────────
|
||||
foreach (Solution::published()->get() as $s) {
|
||||
$urls[] = [
|
||||
'loc' => route('solutions.show', $s->slug),
|
||||
'last' => $s->updated_at?->toW3CString(),
|
||||
'freq' => 'weekly',
|
||||
'pri' => '0.7',
|
||||
];
|
||||
}
|
||||
|
||||
// ── 动态内容:新闻动态 ────────────────────────
|
||||
foreach (News::published()->get() as $n) {
|
||||
$urls[] = [
|
||||
'loc' => route('news.show', $n->slug),
|
||||
'last' => $n->published_at?->toW3CString(),
|
||||
'freq' => 'monthly',
|
||||
'pri' => '0.6',
|
||||
];
|
||||
}
|
||||
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
||||
|
||||
foreach ($urls as $u) {
|
||||
$xml .= ' <url>' . "\n";
|
||||
$xml .= ' <loc>' . e($u['loc']) . '</loc>' . "\n";
|
||||
|
||||
if (!empty($u['last'])) {
|
||||
$xml .= ' <lastmod>' . e($u['last']) . '</lastmod>' . "\n";
|
||||
}
|
||||
|
||||
$xml .= ' <changefreq>' . e($u['freq']) . '</changefreq>' . "\n";
|
||||
$xml .= ' <priority>' . e($u['pri']) . '</priority>' . "\n";
|
||||
$xml .= ' </url>' . "\n";
|
||||
}
|
||||
|
||||
$xml .= '</urlset>';
|
||||
|
||||
return response($xml, 200, [
|
||||
'Content-Type' => 'application/xml; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* robots.txt:开放前台收录,屏蔽后台与用户接口,并指向 sitemap.xml。
|
||||
* 访问 /robots.txt 获取(动态读取 APP_URL,域名随环境自动变化)。
|
||||
*/
|
||||
public function robots()
|
||||
{
|
||||
$base = rtrim(config('app.url'), '/');
|
||||
|
||||
$content = "User-agent: *\n";
|
||||
$content .= "Allow: /\n\n";
|
||||
$content .= "# 后台与管理 / 用户接口不向搜索引擎开放\n";
|
||||
$content .= "Disallow: /admin/\n";
|
||||
$content .= "Disallow: /login\n";
|
||||
$content .= "Disallow: /register\n";
|
||||
$content .= "Disallow: /logout\n";
|
||||
$content .= "Disallow: /settings\n";
|
||||
$content .= "Disallow: /upload\n";
|
||||
$content .= "Disallow: /forum/threads\n";
|
||||
$content .= "Disallow: /api/\n\n";
|
||||
$content .= "Sitemap: {$base}/sitemap.xml\n";
|
||||
|
||||
return response($content, 200, [
|
||||
'Content-Type' => 'text/plain; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可视化「网站地图」页面(用户可见,列出全站主要页面与内容)。
|
||||
* 访问 /sitemap 获取。
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$products = Product::published()->get();
|
||||
$articles = Article::published()->get();
|
||||
$terms = GlossaryTerm::ordered()->get();
|
||||
$solutions = Solution::published()->get();
|
||||
$news = News::published()->get();
|
||||
|
||||
return view('sitemap', compact('products', 'articles', 'terms', 'solutions', 'news'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Solution;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SolutionsController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$power = $request->query('power');
|
||||
$application = $request->query('application');
|
||||
$platform = $request->query('platform');
|
||||
|
||||
$solutions = Solution::published()
|
||||
->when($power, fn ($q) => $q->where('power_segment', $power))
|
||||
->when($application, fn ($q) => $q->where('application', $application))
|
||||
->when($platform, fn ($q) => $q->where('chip_platform', $platform))
|
||||
->get();
|
||||
|
||||
return view('solutions.index', compact('solutions', 'power', 'application', 'platform'));
|
||||
}
|
||||
|
||||
public function show(Solution $solution): View
|
||||
{
|
||||
if ($solution->status !== 'published') {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('solutions.show', compact('solution'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* 通用图片上传:登录用户可用(文章正文插图、封面图)。
|
||||
* 存入 storage/app/public/uploads/{Y}/{m}/,经已建立的 storage:link 暴露为 /storage/uploads/...
|
||||
*/
|
||||
class UploadController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'file' => ['required', 'file', 'image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
|
||||
]);
|
||||
|
||||
$path = $request->file('file')->store('uploads/' . date('Y/m'), 'public');
|
||||
|
||||
return response()->json([
|
||||
'url' => Storage::disk('public')->url($path),
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Admin
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (! $request->user()) {
|
||||
return redirect()->route('admin.login');
|
||||
}
|
||||
|
||||
if (! $request->user()->isAdmin()) {
|
||||
abort(403, '需要管理员权限');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 权限点门禁:can:articles.moderate
|
||||
* 传入多个权限点时,拥有任一即通过(OR 语义)。
|
||||
*/
|
||||
class Can
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string ...$permissions): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user) {
|
||||
abort(403, '需要登录');
|
||||
}
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
if ($user->canPerm($permission)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
abort(403, '无访问权限');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 角色门禁:role:super_admin,admin,forum_admin,staff
|
||||
* 用于「能否进入后台」这类按角色层级的判断。
|
||||
*/
|
||||
class Role
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string ...$roles): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user || ! in_array($user->role, $roles, true)) {
|
||||
abort(403, '无访问权限');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 统一下发 HTTP 安全响应头(纵深防御)。
|
||||
*
|
||||
* 设计说明(零构建架构约束):
|
||||
* - 本站前台使用 Tailwind Play CDN(cdn.tailwindcss.com)与内联 tailwind.config,
|
||||
* 因此 script-src / style-src 必须放行该 CDN 与 'unsafe-inline'。
|
||||
* - 若日后将 Tailwind 改为构建产物并为内联脚本加 nonce,可移除 'unsafe-inline' 收紧 CSP。
|
||||
* - HSTS 仅在「生产环境 + 已启用 HTTPS」时下发,避免本地 http 下浏览器记忆错误。
|
||||
*/
|
||||
class SecurityHeaders
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// 禁止 MIME 嗅探(防止被当作可执行内容)
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// 防点击劫持(现代浏览器看 frame-ancestors,旧浏览器看 X-Frame-Options)
|
||||
$response->headers->set('X-Frame-Options', 'DENY');
|
||||
|
||||
// Referrer 泄露控制
|
||||
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// 关闭不必要的浏览器敏感能力
|
||||
$response->headers->set(
|
||||
'Permissions-Policy',
|
||||
"geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()"
|
||||
);
|
||||
|
||||
// 内容安全策略:限制脚本/样式/连接/表单来源,缩小 XSS 影响面
|
||||
$csp = implode('; ', [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com",
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com",
|
||||
"img-src 'self' data: https:",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
]);
|
||||
$response->headers->set('Content-Security-Policy', $csp);
|
||||
|
||||
// HSTS:仅生产 + HTTPS 时下发
|
||||
if ($request->isSecure() && app()->environment('production')) {
|
||||
$response->headers->set(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
);
|
||||
}
|
||||
|
||||
// 禁止代理/浏览器缓存动态页面(含 CSRF token 的表单页)。
|
||||
// 否则 nginx/浏览器缓存的旧 HTML 会携带过期 _token,提交即触发 419 Page Expired。
|
||||
// 静态资源由 web 服务器直接服务、不经过本中间件,不受影响。
|
||||
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
|
||||
$response->headers->set('Pragma', 'no-cache');
|
||||
$response->headers->set('Expires', '0');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Article extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'category_id',
|
||||
'user_id',
|
||||
'title',
|
||||
'slug',
|
||||
'summary',
|
||||
'body',
|
||||
'cover',
|
||||
'author',
|
||||
'status',
|
||||
'template',
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'category_id' => 'integer',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
public const TEMPLATES = [
|
||||
'standard' => '标准文章',
|
||||
'tech' => '技术文档',
|
||||
'feature' => '专题报道',
|
||||
];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'category_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 可见性作用域(与 forum 一致):
|
||||
* - 审核员(articles.moderate):看全部
|
||||
* - 登录用户:看已审 + 自己投稿(无论状态)
|
||||
* - 游客:仅看已审
|
||||
*/
|
||||
public function scopeVisibleTo($query, ?User $user = null)
|
||||
{
|
||||
if ($user && $user->canPerm('articles.moderate')) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
return $query->where(function ($q) use ($user) {
|
||||
$q->where('status', 'approved')->orWhere('user_id', $user->id);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->where('status', 'approved');
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'approved')
|
||||
->whereNotNull('published_at')
|
||||
->orderByDesc('published_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台可见文章:已审核通过(approved) 且已设定发布时间。
|
||||
* 用户自己的待审(pending)/驳回(rejected) 由控制器单独查询,不进此作用域。
|
||||
*/
|
||||
public function scopeVisiblePublic($query)
|
||||
{
|
||||
return $query->where('status', 'approved')
|
||||
->whereNotNull('published_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台 SEO 标题:优先用文章自定义,否则回退到标题。
|
||||
*/
|
||||
public function seoTitle(): string
|
||||
{
|
||||
return $this->seo_title ?: $this->title;
|
||||
}
|
||||
|
||||
public function seoDescription(): string
|
||||
{
|
||||
return $this->seo_description ?: ($this->summary ?: $this->title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'type',
|
||||
'parent_id',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
|
||||
public function products(): HasMany
|
||||
{
|
||||
return $this->hasMany(Product::class, 'category_id');
|
||||
}
|
||||
|
||||
public function articles(): HasMany
|
||||
{
|
||||
return $this->hasMany(Article::class, 'category_id');
|
||||
}
|
||||
|
||||
public function scopeOfType($query, string $type)
|
||||
{
|
||||
return $query->where('type', $type)->orderBy('sort')->orderBy('name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ContactMessage extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'company',
|
||||
'subject',
|
||||
'message',
|
||||
'ip',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'ip',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ForumBoard extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort' => 'integer',
|
||||
];
|
||||
|
||||
public function threads(): HasMany
|
||||
{
|
||||
return $this->hasMany(ForumThread::class, 'board_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ForumReply extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'thread_id',
|
||||
'user_id',
|
||||
'body',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'thread_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_APPROVED = 'approved';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
|
||||
/**
|
||||
* 可见性作用域(同 ForumThread):
|
||||
* - 审核员:全部
|
||||
* - 登录用户:已审 + 自己发的
|
||||
* - 游客:仅已审
|
||||
*/
|
||||
public function scopeVisible($query, ?User $user = null)
|
||||
{
|
||||
if ($user && $user->canPerm('forum.moderate')) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
return $query->where(function ($q) use ($user) {
|
||||
$q->where('status', self::STATUS_APPROVED)
|
||||
->orWhere('user_id', $user->id);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->where('status', self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
public function thread(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ForumThread::class, 'thread_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ForumThread extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'board_id',
|
||||
'user_id',
|
||||
'title',
|
||||
'body',
|
||||
'status',
|
||||
'is_pinned',
|
||||
'is_locked',
|
||||
'views',
|
||||
'last_reply_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'board_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'is_pinned' => 'boolean',
|
||||
'is_locked' => 'boolean',
|
||||
'views' => 'integer',
|
||||
'last_reply_at' => 'datetime',
|
||||
];
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_APPROVED = 'approved';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
|
||||
/**
|
||||
* 可见性作用域:
|
||||
* - 审核员(forum.moderate):看全部(含待审/驳回)
|
||||
* - 登录用户:看已审 + 自己发的(无论状态)
|
||||
* - 游客:仅看已审
|
||||
*/
|
||||
public function scopeVisible($query, ?User $user = null)
|
||||
{
|
||||
if ($user && $user->canPerm('forum.moderate')) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
return $query->where(function ($q) use ($user) {
|
||||
$q->where('status', self::STATUS_APPROVED)
|
||||
->orWhere('user_id', $user->id);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->where('status', self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
public function board(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ForumBoard::class, 'board_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function replies(): HasMany
|
||||
{
|
||||
return $this->hasMany(ForumReply::class, 'thread_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GlossaryTerm extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'term',
|
||||
'slug',
|
||||
'definition',
|
||||
'aliases',
|
||||
'category',
|
||||
];
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('term');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HeroSlide extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'location',
|
||||
'title',
|
||||
'content',
|
||||
'cta_text',
|
||||
'cta_url',
|
||||
'cta2_text',
|
||||
'cta2_url',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeLocation(Builder $query, string $location): Builder
|
||||
{
|
||||
return $query->where('location', $location);
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NavigationMenu extends Model
|
||||
{
|
||||
protected $fillable = ['label', 'url', 'is_visible', 'sort_order'];
|
||||
|
||||
protected $casts = [
|
||||
'is_visible' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 前台可见菜单:按排序返回。
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_visible', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class News extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'category',
|
||||
'summary',
|
||||
'body',
|
||||
'cover',
|
||||
'status',
|
||||
'published_at',
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
public const CATEGORIES = [
|
||||
'industry' => '行业资讯',
|
||||
'company' => '公司动态',
|
||||
'event' => '展会活动',
|
||||
];
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published')
|
||||
->whereNotNull('published_at')
|
||||
->orderByDesc('published_at');
|
||||
}
|
||||
|
||||
public function categoryLabel(): string
|
||||
{
|
||||
return self::CATEGORIES[$this->category] ?? $this->category ?? '—';
|
||||
}
|
||||
|
||||
public function seoTitle(): string
|
||||
{
|
||||
return $this->seo_title ?: $this->title;
|
||||
}
|
||||
|
||||
public function seoDescription(): string
|
||||
{
|
||||
return $this->seo_description ?: ($this->summary ?: $this->title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'category_id',
|
||||
'name',
|
||||
'slug',
|
||||
'model',
|
||||
'summary',
|
||||
'description',
|
||||
'specs',
|
||||
'cover',
|
||||
'status',
|
||||
'sort',
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'category_id' => 'integer',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'category_id');
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published')->orderBy('sort')->orderBy('name');
|
||||
}
|
||||
|
||||
public function seoTitle(): string
|
||||
{
|
||||
return $this->seo_title ?: ($this->name . ' - 深圳市云创芯电子有限公司');
|
||||
}
|
||||
|
||||
public function seoDescription(): string
|
||||
{
|
||||
return $this->seo_description ?: ($this->summary ?: $this->name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
protected $fillable = ['key', 'value', 'group'];
|
||||
|
||||
/**
|
||||
* 读取设置值,未设置时返回默认值。
|
||||
*/
|
||||
public static function get(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$row = static::where('key', $key)->first();
|
||||
if (!$row) {
|
||||
return $default;
|
||||
}
|
||||
return $row->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入或更新设置值。
|
||||
*/
|
||||
public static function set(string $key, ?string $value, string $group = 'general'): void
|
||||
{
|
||||
static::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $value, 'group' => $group]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Solution extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'power_segment',
|
||||
'application',
|
||||
'chip_platform',
|
||||
'power',
|
||||
'summary',
|
||||
'advantages',
|
||||
'description',
|
||||
'cover',
|
||||
'status',
|
||||
'sort',
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
];
|
||||
|
||||
public const POWER_SEGMENTS = [
|
||||
'lt30' => '30W 以下',
|
||||
'30-65' => '30–65W',
|
||||
'65-100' => '65–100W',
|
||||
'gt100' => '100W 以上',
|
||||
];
|
||||
|
||||
public const APPLICATIONS = [
|
||||
'charger' => '充电器',
|
||||
'powerbank' => '移动电源',
|
||||
'car' => '车载充电',
|
||||
'dock' => '扩展坞 / 多口排插',
|
||||
];
|
||||
|
||||
public const CHIP_PLATFORMS = [
|
||||
'protocol' => '协议芯片',
|
||||
'soc' => '高集成 SoC',
|
||||
'rectifier' => '同步整流',
|
||||
];
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published')->orderBy('sort')->orderBy('name');
|
||||
}
|
||||
|
||||
public function powerSegmentLabel(): string
|
||||
{
|
||||
return self::POWER_SEGMENTS[$this->power_segment] ?? $this->power_segment ?? '—';
|
||||
}
|
||||
|
||||
public function applicationLabel(): string
|
||||
{
|
||||
return self::APPLICATIONS[$this->application] ?? $this->application ?? '—';
|
||||
}
|
||||
|
||||
public function chipPlatformLabel(): string
|
||||
{
|
||||
return self::CHIP_PLATFORMS[$this->chip_platform] ?? $this->chip_platform ?? '—';
|
||||
}
|
||||
|
||||
public function advantagesList(): array
|
||||
{
|
||||
if (! $this->advantages) {
|
||||
return [];
|
||||
}
|
||||
return collect(explode("\n", $this->advantages))
|
||||
->map(fn ($line) => trim($line))
|
||||
->filter()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function seoTitle(): string
|
||||
{
|
||||
return $this->seo_title ?: ($this->name . ' - 深圳市云创芯电子有限公司');
|
||||
}
|
||||
|
||||
public function seoDescription(): string
|
||||
{
|
||||
return $this->seo_description ?: ($this->summary ?: $this->name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'is_admin',
|
||||
'role',
|
||||
'permissions',
|
||||
'points',
|
||||
'real_name',
|
||||
'phone',
|
||||
'company',
|
||||
'industry',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_admin' => 'boolean',
|
||||
'role' => 'string',
|
||||
'permissions' => 'array',
|
||||
'points' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 等级阈值(积分下限),键名即等级 L1..L5
|
||||
*/
|
||||
public const LEVELS = [
|
||||
1 => 0,
|
||||
2 => 50,
|
||||
3 => 200,
|
||||
4 => 600,
|
||||
5 => 1500,
|
||||
];
|
||||
|
||||
public const LEVEL_NAMES = [
|
||||
1 => '新芯用户',
|
||||
2 => '活跃用户',
|
||||
3 => '资深用户',
|
||||
4 => '核心用户',
|
||||
5 => '云创芯达人',
|
||||
];
|
||||
|
||||
/**
|
||||
* 兼容旧逻辑:有后台角色即视为「管理员」(用于前台「后台」入口判断)。
|
||||
* 新逻辑请使用 role() / canPerm()。
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role !== 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色等级(1-5),用于层级判断。
|
||||
*/
|
||||
public function roleLevel(): int
|
||||
{
|
||||
return (int) config("permissions.roles.{$this->role}.level", 1);
|
||||
}
|
||||
|
||||
public function roleLabel(): string
|
||||
{
|
||||
return config("permissions.roles.{$this->role}.label", '注册用户');
|
||||
}
|
||||
|
||||
public function isSuperAdmin(): bool
|
||||
{
|
||||
return $this->role === 'super_admin';
|
||||
}
|
||||
|
||||
public function isStaff(): bool
|
||||
{
|
||||
return $this->role === 'staff';
|
||||
}
|
||||
|
||||
/**
|
||||
* 细粒度权限判定。
|
||||
* - 超级管理员:恒 true
|
||||
* - 内部员工:查 users.permissions(管理员勾选分配)
|
||||
* - 其他角色:查 config/permissions.php 中该角色的默认权限集
|
||||
*/
|
||||
public function canPerm(string $key): bool
|
||||
{
|
||||
if ($this->role === 'super_admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->role === 'staff') {
|
||||
$perms = is_array($this->permissions) ? $this->permissions : [];
|
||||
return in_array($key, $perms, true);
|
||||
}
|
||||
|
||||
$def = config("permissions.roles.{$this->role}.permissions", []);
|
||||
if (in_array('*', $def, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($key, $def, true);
|
||||
}
|
||||
|
||||
public function threads()
|
||||
{
|
||||
return $this->hasMany(ForumThread::class);
|
||||
}
|
||||
|
||||
public function replies()
|
||||
{
|
||||
return $this->hasMany(ForumReply::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加积分(用于注册、发帖、回帖等动作)。
|
||||
*/
|
||||
public function awardPoints(int $amount): void
|
||||
{
|
||||
$this->increment('points', max(0, $amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前等级(1-5)。
|
||||
*/
|
||||
public function level(): int
|
||||
{
|
||||
$points = $this->points ?? 0;
|
||||
$level = 1;
|
||||
foreach (self::LEVELS as $lv => $threshold) {
|
||||
if ($points >= $threshold) {
|
||||
$level = $lv;
|
||||
}
|
||||
}
|
||||
return $level;
|
||||
}
|
||||
|
||||
public function levelName(): string
|
||||
{
|
||||
return self::LEVEL_NAMES[$this->level()] ?? '用户';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前等级进度(用于前台进度条展示)。
|
||||
*/
|
||||
public function levelProgress(): array
|
||||
{
|
||||
$level = $this->level();
|
||||
$current = self::LEVELS[$level] ?? 0;
|
||||
$next = self::LEVELS[$level + 1] ?? null;
|
||||
|
||||
if ($next === null) {
|
||||
return ['level' => $level, 'current' => $this->points, 'next' => null, 'percent' => 100];
|
||||
}
|
||||
|
||||
$span = $next - $current;
|
||||
$done = $this->points - $current;
|
||||
$percent = $span > 0 ? (int) min(100, round($done / $span * 100)) : 100;
|
||||
|
||||
return ['level' => $level, 'current' => $this->points, 'next' => $next, 'percent' => $percent];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
\Illuminate\Support\Facades\Blade::if('canPerm', function (?string $permission = null) {
|
||||
$user = auth()->user();
|
||||
if (! $user) {
|
||||
return false;
|
||||
}
|
||||
return $permission === null ? $user->role !== 'user' : $user->canPerm($permission);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
if (file_exists($maintenance = __DIR__.'/storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
|
||||
exit($kernel->handle(new Symfony\Component\Console\Input\ArgvInput));
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware->alias([
|
||||
'admin' => \App\Http\Middleware\Admin::class,
|
||||
'role' => \App\Http\Middleware\Role::class,
|
||||
'can' => \App\Http\Middleware\Can::class,
|
||||
]);
|
||||
|
||||
// 统一安全响应头(纵深防御):对所有 web 路由生效
|
||||
$middleware->appendToGroup('web', \App\Http\Middleware\SecurityHeaders::class);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
})->create();
|
||||
Vendored
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
<?php return array (
|
||||
'laravel/tinker' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||
),
|
||||
),
|
||||
'mews/purifier' =>
|
||||
array (
|
||||
'aliases' =>
|
||||
array (
|
||||
'Purifier' => 'Mews\\Purifier\\Facades\\Purifier',
|
||||
),
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Mews\\Purifier\\PurifierServiceProvider',
|
||||
),
|
||||
),
|
||||
'nesbot/carbon' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Carbon\\Laravel\\ServiceProvider',
|
||||
),
|
||||
),
|
||||
'nunomaduro/termwind' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||
),
|
||||
),
|
||||
'simplesoftwareio/simple-qrcode' =>
|
||||
array (
|
||||
'aliases' =>
|
||||
array (
|
||||
'QrCode' => 'SimpleSoftwareIO\\QrCode\\Facades\\QrCode',
|
||||
),
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider',
|
||||
),
|
||||
),
|
||||
);
|
||||
Vendored
+255
@@ -0,0 +1,255 @@
|
||||
<?php return array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Illuminate\\Auth\\AuthServiceProvider',
|
||||
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
2 => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
3 => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
|
||||
6 => 'Illuminate\\Cookie\\CookieServiceProvider',
|
||||
7 => 'Illuminate\\Database\\DatabaseServiceProvider',
|
||||
8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
|
||||
9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
|
||||
10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
|
||||
11 => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||
12 => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
|
||||
14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
|
||||
15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||
16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||
17 => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
18 => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||
19 => 'Illuminate\\Session\\SessionServiceProvider',
|
||||
20 => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||
21 => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
22 => 'Illuminate\\View\\ViewServiceProvider',
|
||||
23 => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||
24 => 'Mews\\Purifier\\PurifierServiceProvider',
|
||||
25 => 'Carbon\\Laravel\\ServiceProvider',
|
||||
26 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||
27 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider',
|
||||
28 => 'App\\Providers\\AppServiceProvider',
|
||||
),
|
||||
'eager' =>
|
||||
array (
|
||||
0 => 'Illuminate\\Auth\\AuthServiceProvider',
|
||||
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
|
||||
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
|
||||
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
|
||||
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
|
||||
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
|
||||
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
|
||||
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
|
||||
8 => 'Illuminate\\Session\\SessionServiceProvider',
|
||||
9 => 'Illuminate\\View\\ViewServiceProvider',
|
||||
10 => 'Mews\\Purifier\\PurifierServiceProvider',
|
||||
11 => 'Carbon\\Laravel\\ServiceProvider',
|
||||
12 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||
13 => 'SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider',
|
||||
14 => 'App\\Providers\\AppServiceProvider',
|
||||
),
|
||||
'deferred' =>
|
||||
array (
|
||||
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
|
||||
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||
),
|
||||
'when' =>
|
||||
array (
|
||||
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Bus\\BusServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Cache\\CacheServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Concurrency\\ConcurrencyServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Hashing\\HashServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Mail\\MailServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Queue\\QueueServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Redis\\RedisServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Translation\\TranslationServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Validation\\ValidationServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Laravel\\Tinker\\TinkerServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "cloud-chip/laravel-vue",
|
||||
"type": "project",
|
||||
"description": "云创芯官网 - Laravel 12 后端 + Vue3(CDN) 前端,宝塔 PHP8.5/MySQL5.7 适用,零构建零Node",
|
||||
"keywords": ["laravel", "vue", "baota", "mysql5.7"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10",
|
||||
"mews/purifier": "^3.4",
|
||||
"simplesoftwareio/simple-qrcode": "^4.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pint": "^1.18",
|
||||
"mockery/mockery": "^1.6",
|
||||
"phpunit/phpunit": "^11.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"format": "pint"
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"platform": {
|
||||
"php": "8.5.0"
|
||||
}
|
||||
}
|
||||
Generated
+8322
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'name' => env('APP_NAME', 'CloudChip'),
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
'asset_url' => env('ASSET_URL'),
|
||||
|
||||
'timezone' => 'Asia/Shanghai',
|
||||
|
||||
'locale' => 'zh_CN',
|
||||
|
||||
'fallback_locale' => 'en',
|
||||
|
||||
'faker_locale' => 'zh_CN',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => 'file',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'defaults' => [
|
||||
'guard' => 'web',
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => App\Models\User::class,
|
||||
],
|
||||
],
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => 'password_reset_tokens',
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
'password_timeout' => 10800,
|
||||
|
||||
];
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('CACHE_DRIVER', 'file'),
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'cache',
|
||||
'connection' => null,
|
||||
'lock_connection' => null,
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'cache',
|
||||
'lock_connection' => 'default',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', 'cloudchip_cache'),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => [env('APP_URL', 'http://localhost')],
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
'exposed_headers' => [],
|
||||
|
||||
'max_age' => 0,
|
||||
|
||||
'supports_credentials' => true,
|
||||
|
||||
];
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
// 宝塔 MySQL5.7 不支持 utf8mb4_0900_ai_ci,必须用 utf8mb4_unicode_ci
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
'migrations' => 'migrations',
|
||||
|
||||
'redis' => [
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||
],
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app'),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => false,
|
||||
],
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['single'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'warning'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'warning'),
|
||||
'days' => 14,
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'warning'),
|
||||
'handler' => StreamHandler::class,
|
||||
'with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
'mailers' => [
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
|
||||
'port' => env('MAIL_PORT', 587),
|
||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'mailgun' => [
|
||||
'transport' => 'mailgun',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => ['smtp', 'log'],
|
||||
],
|
||||
],
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@cloud-chip.cn'),
|
||||
'name' => env('MAIL_FROM_NAME', 'CloudChip'),
|
||||
],
|
||||
|
||||
'markdown' => [
|
||||
'theme' => 'default',
|
||||
'paths' => [
|
||||
resource_path('views/vendor/mail'),
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RBAC 权限注册表。
|
||||
*
|
||||
* - groups:权限分组(用于后台「用户权限」勾选界面分组)。
|
||||
* - permissions:全部权限点(key => label + group)。
|
||||
* - roles:角色默认权限。super_admin 用 '*' 表示全部;
|
||||
* staff 默认空,由管理员在用户维度勾选分配(写入 users.permissions JSON)。
|
||||
*
|
||||
* 解析规则见 App\Models\User::canPerm()。
|
||||
*/
|
||||
|
||||
return [
|
||||
'groups' => [
|
||||
'dashboard' => '仪表盘',
|
||||
'content' => '内容管理',
|
||||
'forum' => '论坛管理',
|
||||
'appearance'=> '外观管理',
|
||||
'users' => '用户与权限',
|
||||
'settings' => '站点设置',
|
||||
],
|
||||
|
||||
'permissions' => [
|
||||
// 仪表盘
|
||||
'dashboard.view' => ['label' => '查看仪表盘', 'group' => 'dashboard'],
|
||||
|
||||
// 产品
|
||||
'products.view' => ['label' => '查看产品', 'group' => 'content'],
|
||||
'products.create' => ['label' => '新建产品', 'group' => 'content'],
|
||||
'products.update' => ['label' => '编辑产品', 'group' => 'content'],
|
||||
'products.delete' => ['label' => '删除产品', 'group' => 'content'],
|
||||
|
||||
// 分类
|
||||
'categories.view' => ['label' => '查看分类', 'group' => 'content'],
|
||||
'categories.create' => ['label' => '新建分类', 'group' => 'content'],
|
||||
'categories.update' => ['label' => '编辑分类', 'group' => 'content'],
|
||||
'categories.delete' => ['label' => '删除分类', 'group' => 'content'],
|
||||
|
||||
// 文章
|
||||
'articles.view' => ['label' => '查看文章', 'group' => 'content'],
|
||||
'articles.create' => ['label' => '新建文章', 'group' => 'content'],
|
||||
'articles.update' => ['label' => '编辑文章', 'group' => 'content'],
|
||||
'articles.delete' => ['label' => '删除文章', 'group' => 'content'],
|
||||
'articles.moderate' => ['label' => '审核用户投稿', 'group' => 'content'],
|
||||
|
||||
// 解决方案
|
||||
'solutions.view' => ['label' => '查看方案', 'group' => 'content'],
|
||||
'solutions.create' => ['label' => '新建方案', 'group' => 'content'],
|
||||
'solutions.update' => ['label' => '编辑方案', 'group' => 'content'],
|
||||
'solutions.delete' => ['label' => '删除方案', 'group' => 'content'],
|
||||
|
||||
// 新闻动态
|
||||
'news.view' => ['label' => '查看新闻', 'group' => 'content'],
|
||||
'news.create' => ['label' => '新建新闻', 'group' => 'content'],
|
||||
'news.update' => ['label' => '编辑新闻', 'group' => 'content'],
|
||||
'news.delete' => ['label' => '删除新闻', 'group' => 'content'],
|
||||
|
||||
// 术语库
|
||||
'glossary.view' => ['label' => '查看术语', 'group' => 'content'],
|
||||
'glossary.create' => ['label' => '新建术语', 'group' => 'content'],
|
||||
'glossary.update' => ['label' => '编辑术语', 'group' => 'content'],
|
||||
'glossary.delete' => ['label' => '删除术语', 'group' => 'content'],
|
||||
|
||||
// 论坛
|
||||
'forum.view' => ['label' => '查看论坛', 'group' => 'forum'],
|
||||
'forum.create' => ['label' => '新建板块', 'group' => 'forum'],
|
||||
'forum.update' => ['label' => '编辑板块', 'group' => 'forum'],
|
||||
'forum.delete' => ['label' => '删除板块', 'group' => 'forum'],
|
||||
'forum.moderate' => ['label' => '审核帖子/回复', 'group' => 'forum'],
|
||||
|
||||
// 留言
|
||||
'contacts.view' => ['label' => '查看留言', 'group' => 'users'],
|
||||
'contacts.delete' => ['label' => '删除留言', 'group' => 'users'],
|
||||
|
||||
// 外观管理(首页轮播 / 导航菜单)
|
||||
'appearance.view' => ['label' => '查看外观设置', 'group' => 'appearance'],
|
||||
'appearance.update' => ['label' => '修改外观设置', 'group' => 'appearance'],
|
||||
|
||||
// 用户与权限
|
||||
'users.view' => ['label' => '查看用户', 'group' => 'users'],
|
||||
'users.create' => ['label' => '新增用户', 'group' => 'users'],
|
||||
'users.update' => ['label' => '编辑用户', 'group' => 'users'],
|
||||
'users.delete' => ['label' => '删除用户', 'group' => 'users'],
|
||||
'users.assign_role' => ['label' => '分配角色/权限', 'group' => 'users'],
|
||||
'users.reset_password' => ['label' => '重置用户密码', 'group' => 'users'],
|
||||
|
||||
// 站点设置
|
||||
'settings.view' => ['label' => '查看站点设置', 'group' => 'settings'],
|
||||
'settings.update' => ['label' => '修改站点设置', 'group' => 'settings'],
|
||||
],
|
||||
|
||||
'roles' => [
|
||||
'super_admin' => [
|
||||
'label' => '超级管理员',
|
||||
'level' => 5,
|
||||
'permissions' => ['*'],
|
||||
],
|
||||
'admin' => [
|
||||
'label' => '管理员',
|
||||
'level' => 4,
|
||||
'permissions' => [
|
||||
'dashboard.view',
|
||||
'products.view', 'products.create', 'products.update', 'products.delete',
|
||||
'categories.view', 'categories.create', 'categories.update', 'categories.delete',
|
||||
'articles.view', 'articles.create', 'articles.update', 'articles.delete', 'articles.moderate',
|
||||
'solutions.view', 'solutions.create', 'solutions.update', 'solutions.delete',
|
||||
'news.view', 'news.create', 'news.update', 'news.delete',
|
||||
'glossary.view', 'glossary.create', 'glossary.update', 'glossary.delete',
|
||||
'forum.view', 'forum.create', 'forum.update', 'forum.delete', 'forum.moderate',
|
||||
'contacts.view', 'contacts.delete',
|
||||
'appearance.view', 'appearance.update',
|
||||
'users.view', 'users.create', 'users.update', 'users.delete', 'users.assign_role', 'users.reset_password',
|
||||
'settings.view', 'settings.update',
|
||||
],
|
||||
],
|
||||
'forum_admin' => [
|
||||
'label' => '论坛管理员',
|
||||
'level' => 3,
|
||||
'permissions' => [
|
||||
'dashboard.view',
|
||||
'forum.view', 'forum.create', 'forum.update', 'forum.delete', 'forum.moderate',
|
||||
'articles.view', 'articles.moderate',
|
||||
'contacts.view',
|
||||
],
|
||||
],
|
||||
'staff' => [
|
||||
'label' => '内部员工',
|
||||
'level' => 2,
|
||||
'permissions' => [], // 由管理员在「用户权限」中勾选分配
|
||||
],
|
||||
'user' => [
|
||||
'label' => '注册用户',
|
||||
'level' => 1,
|
||||
'permissions' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* HTMLPurifier 配置(mews/purifier ^3.4)。
|
||||
*
|
||||
* 文章正文为富文本(Quill 产出的 HTML),保存前统一经 Purifier 净化,
|
||||
* 剥离 script/style/on* 等危险内容,仅保留排版与图片所需标签/属性。
|
||||
*
|
||||
* 使用:Purifier::clean($html, 'article')。'default' 作为兜底档。
|
||||
* Cache.DefinitionImpl 置 null:关闭定义缓存,省去可写缓存目录,部署更省心。
|
||||
*/
|
||||
|
||||
return [
|
||||
'settings' => [
|
||||
'default' => [
|
||||
'HTML.Doctype' => 'HTML 4.01 Transitional',
|
||||
'HTML.Allowed' => 'div,b,strong,i,em,u,a[href|title|target|rel],ul,ol,li,p,br,span,img[src|alt|width|height|class],blockquote,pre,code,table,thead,tbody,tr,th,td,h1,h2,h3,h4,h5,h6,hr,small,sub,sup',
|
||||
'CSS.AllowedProperties' => 'text-align,float,margin,width,height,max-width,color,background-color,font-size,font-weight,font-style',
|
||||
'AutoFormat.AutoParagraph' => false,
|
||||
'AutoFormat.RemoveEmpty' => true,
|
||||
'Attr.AllowedFrameTargets' => ['_blank'],
|
||||
'Attr.AllowedRel' => ['nofollow', 'noopener', 'noreferrer'],
|
||||
'HTML.TargetBlank' => true,
|
||||
'URI.AllowedSchemes' => ['http', 'https', 'mailto', 'tel', 'data'],
|
||||
'URI.MakeAbsolute' => false,
|
||||
'Cache.DefinitionImpl' => null,
|
||||
],
|
||||
'article' => [
|
||||
'HTML.Doctype' => 'HTML 4.01 Transitional',
|
||||
// 后台富文本(Quill)会产出 class:ql-align-*/ql-size-*/ql-indent-* 等,
|
||||
// 同时首页 Hero 默认内容也用 class 排版;故对常用块级/行内标签放开 class,
|
||||
// 仅保留白名单标签与安全的 CSS 属性(style 仍受 CSS.AllowedProperties 约束)。
|
||||
'HTML.Allowed' => 'div[class|style],b[style],strong[style],i[style],em[style],u[style],a[href|title|target|rel|class|style],ul[class|style],ol[class|style],li[class|style],p[class|style],br,span[class|style],img[src|alt|width|height|class|style],blockquote[class|style],pre[class|style],code[class|style],table[class|style],thead[class|style],tbody[class|style],tr[class|style],th[class|style],td[class|style],h1[class|style],h2[class|style],h3[class|style],h4[class|style],h5[class|style],h6[class|style],hr,small[class|style],sub,sup',
|
||||
'CSS.AllowedProperties' => 'text-align,float,margin,margin-left,margin-right,padding,padding-left,width,height,max-width,color,background-color,font-size,font-weight,font-style',
|
||||
'AutoFormat.AutoParagraph' => false,
|
||||
'AutoFormat.RemoveEmpty' => false,
|
||||
'Attr.AllowedFrameTargets' => ['_blank'],
|
||||
'Attr.AllowedRel' => ['nofollow', 'noopener', 'noreferrer'],
|
||||
'HTML.TargetBlank' => true,
|
||||
'URI.AllowedSchemes' => ['http', 'https', 'mailto', 'tel', 'data'],
|
||||
'URI.MakeAbsolute' => false,
|
||||
'Cache.DefinitionImpl' => null,
|
||||
],
|
||||
],
|
||||
'custom_definition' => [],
|
||||
];
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'sync'),
|
||||
|
||||
'connections' => [
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'jobs',
|
||||
'queue' => 'default',
|
||||
'retry_after' => 90,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => 90,
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
],
|
||||
|
||||
'batching' => [
|
||||
'database' => 'mysql',
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'mysql'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'mailgun' => [
|
||||
'domain' => env('MAILGUN_DOMAIN'),
|
||||
'secret' => env('MAILGUN_SECRET'),
|
||||
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
|
||||
'scheme' => 'https',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'file'),
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => false,
|
||||
|
||||
'encrypt' => true,
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
'table' => 'sessions',
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
'cookie' => env('SESSION_COOKIE', 'cloudchip_session'),
|
||||
|
||||
'path' => '/',
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE', true),
|
||||
|
||||
'http_only' => true,
|
||||
|
||||
'same_site' => 'lax',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'paths' => [
|
||||
resource_path('views'),
|
||||
],
|
||||
|
||||
'compiled' => env(
|
||||
'VIEW_COMPILED_PATH',
|
||||
realpath(storage_path('framework/views'))
|
||||
),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('contact_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 40);
|
||||
$table->string('email', 120);
|
||||
$table->string('phone', 20)->nullable();
|
||||
$table->string('subject', 80);
|
||||
$table->text('message');
|
||||
$table->string('ip', 45)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('email');
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('contact_messages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(false)->after('password');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('type')->default('product'); // product | article
|
||||
$table->unsignedBigInteger('parent_id')->nullable();
|
||||
$table->integer('sort')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('category_id')->nullable();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('model')->nullable();
|
||||
$table->string('summary')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->text('specs')->nullable();
|
||||
$table->string('cover')->nullable();
|
||||
$table->string('status')->default('published'); // published | draft
|
||||
$table->integer('sort')->default(0);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->foreign('category_id')->references('id')->on('categories')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('articles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('category_id')->nullable();
|
||||
$table->string('title');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('summary')->nullable();
|
||||
$table->text('body')->nullable();
|
||||
$table->string('cover')->nullable();
|
||||
$table->string('author')->nullable();
|
||||
$table->string('status')->default('published'); // published | draft
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->foreign('category_id')->references('id')->on('categories')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('articles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('glossary_terms', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('term');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('definition');
|
||||
$table->string('aliases')->nullable();
|
||||
$table->string('category')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('glossary_terms');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('forum_boards', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->integer('sort')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_boards');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('forum_threads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('board_id');
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->string('title');
|
||||
$table->text('body');
|
||||
$table->boolean('is_pinned')->default(false);
|
||||
$table->boolean('is_locked')->default(false);
|
||||
$table->unsignedInteger('views')->default(0);
|
||||
$table->timestamp('last_reply_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->foreign('board_id')->references('id')->on('forum_boards')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_threads');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('forum_replies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('thread_id');
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->text('body');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->foreign('thread_id')->references('id')->on('forum_threads')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_replies');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique();
|
||||
$table->text('value')->nullable();
|
||||
$table->string('group')->default('general');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->unsignedInteger('points')->default(0)->after('is_admin');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('points');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('articles', function (Blueprint $table) {
|
||||
$table->string('template')->default('standard')->after('status');
|
||||
$table->string('seo_title')->nullable()->after('template');
|
||||
$table->string('seo_description')->nullable()->after('seo_title');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('articles', function (Blueprint $table) {
|
||||
$table->dropColumn(['template', 'seo_title', 'seo_description']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->string('seo_title')->nullable()->after('slug');
|
||||
$table->string('seo_description')->nullable()->after('seo_title');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn(['seo_title', 'seo_description']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 5 级角色 + 细粒度权限(内部员工由管理员分配)。
|
||||
* 存量用户默认 role='user';种子管理员会在 Seeder 中置为 super_admin。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('role')->default('user')->after('is_admin');
|
||||
$table->json('permissions')->nullable()->after('role');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['role', 'permissions']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 论坛楼/回帖增加审核状态。
|
||||
* 默认 approved:存量内容(迁移前已存在)保持公开,不回流到待审。
|
||||
* 新发布内容由控制器显式置为 pending。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('forum_threads', function (Blueprint $table) {
|
||||
$table->string('status')->default('approved')->after('body');
|
||||
});
|
||||
|
||||
Schema::table('forum_replies', function (Blueprint $table) {
|
||||
$table->string('status')->default('approved')->after('body');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('forum_threads', function (Blueprint $table) {
|
||||
$table->dropColumn('status');
|
||||
});
|
||||
|
||||
Schema::table('forum_replies', function (Blueprint $table) {
|
||||
$table->dropColumn('status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 文章支持用户投稿:增加 user_id(投稿人,管理员发稿为 null)。
|
||||
*
|
||||
* 注意:articles.status 列在 000005 迁移中已存在(原语义 published/draft)。
|
||||
* 此处不再重复加列,而是把语义扩展为统一集:
|
||||
* approved(公开)| pending(用户投稿待审)| rejected(用户投稿驳回)| draft(管理员草稿)
|
||||
* 存量 published 视为已审核公开,统一映射为 approved。
|
||||
*
|
||||
* 容错:user_id 可能已在一次失败的迁移中被 MySQL 隐式提交创建,故加存在性判断。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasColumn('articles', 'user_id')) {
|
||||
Schema::table('articles', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')
|
||||
->nullable()
|
||||
->after('category_id')
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
DB::table('articles')
|
||||
->where('status', 'published')
|
||||
->update(['status' => 'approved']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('articles', 'user_id')) {
|
||||
Schema::table('articles', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('user_id');
|
||||
});
|
||||
}
|
||||
// 不回滚 status 取值,避免误伤已发布内容。
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user