From cc9f6c4d714a3080c00518462801abd7675d5559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com> Date: Tue, 14 Jul 2026 10:09:24 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=B6=E6=AE=B5=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/auto-imports.d.ts | 22 +- backend/components.d.ts | 154 ++--- .../NOTEBOOK_DEPLOYMENT_CHECKLIST.md | 626 +++++++++--------- README.md => docs/README.md | 0 docs/域名系统配置指南_Go.md | 248 +++++++ frontend/src/router/routes.ts | 2 +- go/controllers/index_portal.go | 396 +++++++++++ go/controllers/platform_account_pool.go | 153 ++++- go/routers/index/index.go | 31 +- .../src/views/accountpool/codex/index.vue | 19 +- .../src/views/accountpool/cursor/index.vue | 19 +- platform/src/views/accountpool/kiro/index.vue | 19 +- .../src/views/accountpool/windsurf/index.vue | 19 +- 13 files changed, 1262 insertions(+), 446 deletions(-) rename NOTEBOOK_DEPLOYMENT_CHECKLIST.md => docs/NOTEBOOK_DEPLOYMENT_CHECKLIST.md (95%) rename README.md => docs/README.md (100%) create mode 100644 docs/域名系统配置指南_Go.md create mode 100644 go/controllers/index_portal.go diff --git a/backend/auto-imports.d.ts b/backend/auto-imports.d.ts index 2e1dba4..8c1957b 100644 --- a/backend/auto-imports.d.ts +++ b/backend/auto-imports.d.ts @@ -1,11 +1,11 @@ -/* eslint-disable */ -/* prettier-ignore */ -// @ts-nocheck -// noinspection JSUnusedGlobalSymbols -// Generated by unplugin-auto-import -// biome-ignore lint: disable -export {} -declare global { - const ElMessage: typeof import('element-plus/es').ElMessage - const ElMessageBox: typeof import('element-plus/es').ElMessageBox -} +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const ElMessage: typeof import('element-plus/es').ElMessage + const ElMessageBox: typeof import('element-plus/es').ElMessageBox +} diff --git a/backend/components.d.ts b/backend/components.d.ts index 1bd82ef..3e73949 100644 --- a/backend/components.d.ts +++ b/backend/components.d.ts @@ -1,77 +1,77 @@ -/* eslint-disable */ -// @ts-nocheck -// biome-ignore lint: disable -// oxlint-disable -// ------ -// Generated by unplugin-vue-components -// Read more: https://github.com/vuejs/core/pull/3399 - -export {} - -/* prettier-ignore */ -declare module 'vue' { - export interface GlobalComponents { - CommonAside: typeof import('./src/components/CommonAside.vue')['default'] - CommonHeader: typeof import('./src/components/CommonHeader.vue')['default'] - ElAlert: typeof import('element-plus/es')['ElAlert'] - ElAside: typeof import('element-plus/es')['ElAside'] - ElAvatar: typeof import('element-plus/es')['ElAvatar'] - ElBacktop: typeof import('element-plus/es')['ElBacktop'] - ElBadge: typeof import('element-plus/es')['ElBadge'] - ElButton: typeof import('element-plus/es')['ElButton'] - ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] - ElCard: typeof import('element-plus/es')['ElCard'] - ElCascader: typeof import('element-plus/es')['ElCascader'] - ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] - ElCol: typeof import('element-plus/es')['ElCol'] - ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] - ElContainer: typeof import('element-plus/es')['ElContainer'] - ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] - ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] - ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] - ElDialog: typeof import('element-plus/es')['ElDialog'] - ElDivider: typeof import('element-plus/es')['ElDivider'] - ElDrawer: typeof import('element-plus/es')['ElDrawer'] - ElDropdown: typeof import('element-plus/es')['ElDropdown'] - ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] - ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] - ElEmpty: typeof import('element-plus/es')['ElEmpty'] - ElForm: typeof import('element-plus/es')['ElForm'] - ElFormItem: typeof import('element-plus/es')['ElFormItem'] - ElHeader: typeof import('element-plus/es')['ElHeader'] - ElIcon: typeof import('element-plus/es')['ElIcon'] - ElImage: typeof import('element-plus/es')['ElImage'] - ElInput: typeof import('element-plus/es')['ElInput'] - ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] - ElLink: typeof import('element-plus/es')['ElLink'] - ElMain: typeof import('element-plus/es')['ElMain'] - ElMenu: typeof import('element-plus/es')['ElMenu'] - ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] - ElOption: typeof import('element-plus/es')['ElOption'] - ElPagination: typeof import('element-plus/es')['ElPagination'] - ElProgress: typeof import('element-plus/es')['ElProgress'] - ElRadio: typeof import('element-plus/es')['ElRadio'] - ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] - ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] - ElRow: typeof import('element-plus/es')['ElRow'] - ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] - ElSelect: typeof import('element-plus/es')['ElSelect'] - ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] - ElSwitch: typeof import('element-plus/es')['ElSwitch'] - ElTable: typeof import('element-plus/es')['ElTable'] - ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] - ElTabPane: typeof import('element-plus/es')['ElTabPane'] - ElTabs: typeof import('element-plus/es')['ElTabs'] - ElTag: typeof import('element-plus/es')['ElTag'] - ElTooltip: typeof import('element-plus/es')['ElTooltip'] - ElTree: typeof import('element-plus/es')['ElTree'] - ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] - ElUpload: typeof import('element-plus/es')['ElUpload'] - MessageDetailDialog: typeof import('./src/components/MessageDetailDialog.vue')['default'] - RouterLink: typeof import('vue-router')['RouterLink'] - RouterView: typeof import('vue-router')['RouterView'] - } - export interface GlobalDirectives { - vLoading: typeof import('element-plus/es')['ElLoadingDirective'] - } -} +/* eslint-disable */ +// @ts-nocheck +// biome-ignore lint: disable +// oxlint-disable +// ------ +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 + +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + CommonAside: typeof import('./src/components/CommonAside.vue')['default'] + CommonHeader: typeof import('./src/components/CommonHeader.vue')['default'] + ElAlert: typeof import('element-plus/es')['ElAlert'] + ElAside: typeof import('element-plus/es')['ElAside'] + ElAvatar: typeof import('element-plus/es')['ElAvatar'] + ElBacktop: typeof import('element-plus/es')['ElBacktop'] + ElBadge: typeof import('element-plus/es')['ElBadge'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCascader: typeof import('element-plus/es')['ElCascader'] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCol: typeof import('element-plus/es')['ElCol'] + ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] + ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElHeader: typeof import('element-plus/es')['ElHeader'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElImage: typeof import('element-plus/es')['ElImage'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElLink: typeof import('element-plus/es')['ElLink'] + ElMain: typeof import('element-plus/es')['ElMain'] + ElMenu: typeof import('element-plus/es')['ElMenu'] + ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElProgress: typeof import('element-plus/es')['ElProgress'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElTree: typeof import('element-plus/es')['ElTree'] + ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] + ElUpload: typeof import('element-plus/es')['ElUpload'] + MessageDetailDialog: typeof import('./src/components/MessageDetailDialog.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + } + export interface GlobalDirectives { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } +} diff --git a/NOTEBOOK_DEPLOYMENT_CHECKLIST.md b/docs/NOTEBOOK_DEPLOYMENT_CHECKLIST.md similarity index 95% rename from NOTEBOOK_DEPLOYMENT_CHECKLIST.md rename to docs/NOTEBOOK_DEPLOYMENT_CHECKLIST.md index 5430306..432075b 100644 --- a/NOTEBOOK_DEPLOYMENT_CHECKLIST.md +++ b/docs/NOTEBOOK_DEPLOYMENT_CHECKLIST.md @@ -1,313 +1,313 @@ -# 记事本模块部署检查清单 - -## 📋 部署前检查 - -### 1. 数据库准备 -- [ ] 执行 SQL 文件创建数据表 - ```bash - mysql -u用户名 -p数据库名 < sql/yz_platform_notebook.sql - ``` -- [ ] 验证表创建成功 - ```sql - SHOW TABLES LIKE 'yz_platform_notebook'; - DESC yz_platform_notebook; - ``` - -### 2. 后端代码检查 -- [✅] 模型文件已创建: `go/models/platform_notebook.go` -- [✅] 模型已注册: `go/models/init.go` -- [✅] 控制器已创建: `go/controllers/platform_notebook.go` -- [✅] 路由已注册: `go/routers/platform/platform.go` -- [✅] 代码编译通过 - -### 3. 前端代码检查 -- [✅] API文件已创建: `platform/src/api/notebook.js` -- [✅] 主页面已创建: `platform/src/views/apps/notebook/index.vue` -- [✅] 编辑器组件已创建: `platform/src/views/apps/notebook/components/edit.vue` -- [✅] WangEditor组件已创建: `platform/src/views/apps/notebook/components/WangEditor.vue` - -### 4. 依赖检查 -- [ ] 前端安装 WangEditor - ```bash - cd platform - npm install @wangeditor/editor - # 或 - yarn add @wangeditor/editor - ``` - -### 5. 菜单配置 -- [ ] 在平台管理后台添加记事本菜单项 - - 路径: `/apps/notebook` - - 组件: `apps/notebook/index.vue` - - 图标: `fa-solid fa-book` 或其他合适的图标 - - 标题: `记事本` 或 `我的笔记` - -## 🚀 部署步骤 - -### 后端部署 - -1. **停止服务** - ```bash - # 如果服务正在运行,先停止 - pkill -f "go run main.go" - ``` - -2. **编译代码** - ```bash - cd go - go build -o server - ``` - -3. **启动服务** - ```bash - ./server - # 或使用后台运行 - nohup ./server > server.log 2>&1 & - ``` - -### 前端部署 - -1. **安装依赖** - ```bash - cd platform - npm install - # 或 - yarn install - ``` - -2. **开发模式测试** - ```bash - npm run dev - # 或 - yarn dev - ``` - -3. **生产构建** - ```bash - npm run build - # 或 - yarn build - ``` - -## ✅ 功能测试 - -### 基础功能测试 - -1. **访问页面** - - [ ] 能够正常访问记事本页面 - - [ ] 页面布局正常显示 - - [ ] 左侧列表和右侧编辑器都能正常显示 - -2. **创建笔记** - - [ ] 点击"新建笔记"按钮 - - [ ] 输入标题和内容 - - [ ] 点击"创建"按钮 - - [ ] 创建成功,笔记出现在列表中 - -3. **编辑笔记** - - [ ] 点击列表中的笔记 - - [ ] 笔记内容正确加载到编辑器 - - [ ] 修改标题和内容 - - [ ] 点击"保存"按钮 - - [ ] 保存成功,列表中的笔记信息更新 - -4. **删除笔记** - - [ ] 点击笔记右侧的"更多"按钮 - - [ ] 点击"删除" - - [ ] 确认删除 - - [ ] 笔记从列表中移除 - -5. **搜索笔记** - - [ ] 在搜索框输入关键词 - - [ ] 列表自动过滤显示匹配的笔记 - - [ ] 清空搜索框,显示所有笔记 - -### 富文本编辑器测试 - -1. **文本格式** - - [ ] 加粗、斜体、下划线 - - [ ] 标题(H1-H6) - - [ ] 字体颜色和背景色 - -2. **列表和引用** - - [ ] 有序列表 - - [ ] 无序列表 - - [ ] 引用块 - -3. **插入内容** - - [ ] 插入链接 - - [ ] 插入代码块 - - [ ] 插入表格 - -4. **图片上传** (需要配置上传接口) - - [ ] 点击图片按钮 - - [ ] 选择图片文件 - - [ ] 图片正确上传并显示 - -### API测试 (使用浏览器控制台) - -```javascript -// 1. 在浏览器控制台加载测试脚本 -// 将 test-api.js 的内容粘贴到控制台 - -// 2. 运行完整测试 -await NotebookTest.runFullTest(); - -// 3. 或单独测试各个功能 -await NotebookTest.create(); // 创建笔记 -await NotebookTest.list(); // 获取列表 -await NotebookTest.detail(1); // 获取详情 -await NotebookTest.update(1, {...}); // 更新笔记 -await NotebookTest.delete(1); // 删除笔记 -``` - -## 🐛 常见问题排查 - -### 1. 编译错误 - -**问题**: `cannot use &claims.UserID (value of type *int) as *uint64` - -**解决**: 已修复,使用类型转换 `userID := uint64(claims.UserID)` - ---- - -**问题**: `undefined: PlatformNotebook` - -**解决**: 检查 `go/models/init.go` 中是否已注册模型 - ---- - -### 2. 数据库错误 - -**问题**: 表不存在 - -**解决**: -```sql --- 检查表是否存在 -SHOW TABLES LIKE 'yz_platform_notebook'; - --- 如果不存在,执行 SQL 文件 -SOURCE sql/yz_platform_notebook.sql; -``` - ---- - -**问题**: 字段不存在 - -**解决**: 确认字段名使用 `create_time`, `update_time`, `delete_time` - ---- - -### 3. 前端错误 - -**问题**: WangEditor 未定义 - -**解决**: -```bash -npm install @wangeditor/editor -``` - ---- - -**问题**: API 请求 401 未授权 - -**解决**: -- 检查用户是否已登录 -- 检查 JWT Token 是否有效 -- 检查 Token 是否正确设置在请求头中 - ---- - -**问题**: 笔记列表为空 - -**解决**: -- 检查数据库中是否有数据 -- 检查用户ID是否匹配 -- 查看浏览器控制台和网络请求 - ---- - -### 4. 权限错误 - -**问题**: 无法访问其他用户的笔记 - -**说明**: 这是正常的,每个用户只能访问自己的笔记 - ---- - -## 📊 性能优化建议 - -### 数据库优化 -1. 为常用查询字段添加索引(已添加) - - `user_id` - - `create_time` - - `is_deleted` - -2. 定期清理软删除的数据 - ```sql - -- 删除30天前的软删除数据 - DELETE FROM yz_platform_notebook - WHERE is_deleted = 1 - AND delete_time < DATE_SUB(NOW(), INTERVAL 30 DAY); - ``` - -### 前端优化 -1. 列表分页加载(已实现) -2. 内容预览截取(已实现) -3. 懒加载编辑器组件 -4. 防抖搜索功能(可选) - -### 后端优化 -1. 添加缓存层(Redis) -2. 内容压缩存储 -3. 异步处理大文件 -4. API 限流保护 - -## 🔐 安全建议 - -1. **内容安全** - - 前端显示时做 XSS 过滤 - - 后端存储前做内容校验 - - 限制单篇笔记大小 - -2. **访问控制** - - JWT Token 验证(已实现) - - 用户权限校验(已实现) - - API 频率限制 - -3. **数据备份** - - 定期备份数据库 - - 软删除机制(已实现) - - 版本控制(可选) - -## 📝 后续功能扩展 - -- [ ] 笔记分类/文件夹 -- [ ] 笔记标签 -- [ ] 笔记分享 -- [ ] 导出功能(PDF/Markdown) -- [ ] 版本历史 -- [ ] 协作编辑 -- [ ] 全文搜索 -- [ ] 附件上传 -- [ ] 模板功能 -- [ ] 快捷键支持 - -## ✨ 完成标志 - -- [✅] SQL 表创建成功 -- [✅] 后端代码编译通过 -- [✅] 前端页面正常访问 -- [ ] 所有功能测试通过 -- [ ] 无控制台错误 -- [ ] 性能表现良好 - -## 📞 技术支持 - -如遇到问题,请检查: -1. 浏览器控制台错误信息 -2. 后端服务日志 -3. 数据库连接状态 -4. API 请求响应 - -祝部署顺利!🎉 +# 记事本模块部署检查清单 + +## 📋 部署前检查 + +### 1. 数据库准备 +- [ ] 执行 SQL 文件创建数据表 + ```bash + mysql -u用户名 -p数据库名 < sql/yz_platform_notebook.sql + ``` +- [ ] 验证表创建成功 + ```sql + SHOW TABLES LIKE 'yz_platform_notebook'; + DESC yz_platform_notebook; + ``` + +### 2. 后端代码检查 +- [✅] 模型文件已创建: `go/models/platform_notebook.go` +- [✅] 模型已注册: `go/models/init.go` +- [✅] 控制器已创建: `go/controllers/platform_notebook.go` +- [✅] 路由已注册: `go/routers/platform/platform.go` +- [✅] 代码编译通过 + +### 3. 前端代码检查 +- [✅] API文件已创建: `platform/src/api/notebook.js` +- [✅] 主页面已创建: `platform/src/views/apps/notebook/index.vue` +- [✅] 编辑器组件已创建: `platform/src/views/apps/notebook/components/edit.vue` +- [✅] WangEditor组件已创建: `platform/src/views/apps/notebook/components/WangEditor.vue` + +### 4. 依赖检查 +- [ ] 前端安装 WangEditor + ```bash + cd platform + npm install @wangeditor/editor + # 或 + yarn add @wangeditor/editor + ``` + +### 5. 菜单配置 +- [ ] 在平台管理后台添加记事本菜单项 + - 路径: `/apps/notebook` + - 组件: `apps/notebook/index.vue` + - 图标: `fa-solid fa-book` 或其他合适的图标 + - 标题: `记事本` 或 `我的笔记` + +## 🚀 部署步骤 + +### 后端部署 + +1. **停止服务** + ```bash + # 如果服务正在运行,先停止 + pkill -f "go run main.go" + ``` + +2. **编译代码** + ```bash + cd go + go build -o server + ``` + +3. **启动服务** + ```bash + ./server + # 或使用后台运行 + nohup ./server > server.log 2>&1 & + ``` + +### 前端部署 + +1. **安装依赖** + ```bash + cd platform + npm install + # 或 + yarn install + ``` + +2. **开发模式测试** + ```bash + npm run dev + # 或 + yarn dev + ``` + +3. **生产构建** + ```bash + npm run build + # 或 + yarn build + ``` + +## ✅ 功能测试 + +### 基础功能测试 + +1. **访问页面** + - [ ] 能够正常访问记事本页面 + - [ ] 页面布局正常显示 + - [ ] 左侧列表和右侧编辑器都能正常显示 + +2. **创建笔记** + - [ ] 点击"新建笔记"按钮 + - [ ] 输入标题和内容 + - [ ] 点击"创建"按钮 + - [ ] 创建成功,笔记出现在列表中 + +3. **编辑笔记** + - [ ] 点击列表中的笔记 + - [ ] 笔记内容正确加载到编辑器 + - [ ] 修改标题和内容 + - [ ] 点击"保存"按钮 + - [ ] 保存成功,列表中的笔记信息更新 + +4. **删除笔记** + - [ ] 点击笔记右侧的"更多"按钮 + - [ ] 点击"删除" + - [ ] 确认删除 + - [ ] 笔记从列表中移除 + +5. **搜索笔记** + - [ ] 在搜索框输入关键词 + - [ ] 列表自动过滤显示匹配的笔记 + - [ ] 清空搜索框,显示所有笔记 + +### 富文本编辑器测试 + +1. **文本格式** + - [ ] 加粗、斜体、下划线 + - [ ] 标题(H1-H6) + - [ ] 字体颜色和背景色 + +2. **列表和引用** + - [ ] 有序列表 + - [ ] 无序列表 + - [ ] 引用块 + +3. **插入内容** + - [ ] 插入链接 + - [ ] 插入代码块 + - [ ] 插入表格 + +4. **图片上传** (需要配置上传接口) + - [ ] 点击图片按钮 + - [ ] 选择图片文件 + - [ ] 图片正确上传并显示 + +### API测试 (使用浏览器控制台) + +```javascript +// 1. 在浏览器控制台加载测试脚本 +// 将 test-api.js 的内容粘贴到控制台 + +// 2. 运行完整测试 +await NotebookTest.runFullTest(); + +// 3. 或单独测试各个功能 +await NotebookTest.create(); // 创建笔记 +await NotebookTest.list(); // 获取列表 +await NotebookTest.detail(1); // 获取详情 +await NotebookTest.update(1, {...}); // 更新笔记 +await NotebookTest.delete(1); // 删除笔记 +``` + +## 🐛 常见问题排查 + +### 1. 编译错误 + +**问题**: `cannot use &claims.UserID (value of type *int) as *uint64` + +**解决**: 已修复,使用类型转换 `userID := uint64(claims.UserID)` + +--- + +**问题**: `undefined: PlatformNotebook` + +**解决**: 检查 `go/models/init.go` 中是否已注册模型 + +--- + +### 2. 数据库错误 + +**问题**: 表不存在 + +**解决**: +```sql +-- 检查表是否存在 +SHOW TABLES LIKE 'yz_platform_notebook'; + +-- 如果不存在,执行 SQL 文件 +SOURCE sql/yz_platform_notebook.sql; +``` + +--- + +**问题**: 字段不存在 + +**解决**: 确认字段名使用 `create_time`, `update_time`, `delete_time` + +--- + +### 3. 前端错误 + +**问题**: WangEditor 未定义 + +**解决**: +```bash +npm install @wangeditor/editor +``` + +--- + +**问题**: API 请求 401 未授权 + +**解决**: +- 检查用户是否已登录 +- 检查 JWT Token 是否有效 +- 检查 Token 是否正确设置在请求头中 + +--- + +**问题**: 笔记列表为空 + +**解决**: +- 检查数据库中是否有数据 +- 检查用户ID是否匹配 +- 查看浏览器控制台和网络请求 + +--- + +### 4. 权限错误 + +**问题**: 无法访问其他用户的笔记 + +**说明**: 这是正常的,每个用户只能访问自己的笔记 + +--- + +## 📊 性能优化建议 + +### 数据库优化 +1. 为常用查询字段添加索引(已添加) + - `user_id` + - `create_time` + - `is_deleted` + +2. 定期清理软删除的数据 + ```sql + -- 删除30天前的软删除数据 + DELETE FROM yz_platform_notebook + WHERE is_deleted = 1 + AND delete_time < DATE_SUB(NOW(), INTERVAL 30 DAY); + ``` + +### 前端优化 +1. 列表分页加载(已实现) +2. 内容预览截取(已实现) +3. 懒加载编辑器组件 +4. 防抖搜索功能(可选) + +### 后端优化 +1. 添加缓存层(Redis) +2. 内容压缩存储 +3. 异步处理大文件 +4. API 限流保护 + +## 🔐 安全建议 + +1. **内容安全** + - 前端显示时做 XSS 过滤 + - 后端存储前做内容校验 + - 限制单篇笔记大小 + +2. **访问控制** + - JWT Token 验证(已实现) + - 用户权限校验(已实现) + - API 频率限制 + +3. **数据备份** + - 定期备份数据库 + - 软删除机制(已实现) + - 版本控制(可选) + +## 📝 后续功能扩展 + +- [ ] 笔记分类/文件夹 +- [ ] 笔记标签 +- [ ] 笔记分享 +- [ ] 导出功能(PDF/Markdown) +- [ ] 版本历史 +- [ ] 协作编辑 +- [ ] 全文搜索 +- [ ] 附件上传 +- [ ] 模板功能 +- [ ] 快捷键支持 + +## ✨ 完成标志 + +- [✅] SQL 表创建成功 +- [✅] 后端代码编译通过 +- [✅] 前端页面正常访问 +- [ ] 所有功能测试通过 +- [ ] 无控制台错误 +- [ ] 性能表现良好 + +## 📞 技术支持 + +如遇到问题,请检查: +1. 浏览器控制台错误信息 +2. 后端服务日志 +3. 数据库连接状态 +4. API 请求响应 + +祝部署顺利!🎉 diff --git a/README.md b/docs/README.md similarity index 100% rename from README.md rename to docs/README.md diff --git a/docs/域名系统配置指南_Go.md b/docs/域名系统配置指南_Go.md new file mode 100644 index 0000000..7ee5085 --- /dev/null +++ b/docs/域名系统配置指南_Go.md @@ -0,0 +1,248 @@ +# Go (Beego) 多租户二级域名系统部署与配置指南 + +本指南详细介绍在 **不改变你目前宝塔部署路径** 的前提下,如何配置 `yunzer.com.cn` 和 `dh2.fun` 及其所有二级域名的 Nginx,让它们能够直接访问并使用部署在 `www.yunzer.cn` 的官网前端静态模板。 + +--- + +## 🛠️ 第一步:宝塔物理部署路径规划(保持现状) + +请确认或保持你目前在宝塔服务器上的物理目录部署,不需要进行任何文件移动: + +1. **企业官网门户前台 (`frontend`)** + - 物理路径: `/www/wwwroot/www.yunzer.cn` + - 说明: 你的 `frontend` 编译打包文件平铺部署在此目录下(有 `index.html`)。 + +2. **平台运营管理后台 (`platform`)** + - 物理路径: `/www/wwwroot/platform.yunzer.cn` + +3. **租户/商家管理后台 (`back`)** + - 物理路径: `/www/wwwroot/back.yunzer.cn` + +4. **Go 后端 API 服务** + - 运行端口: 本地 `http://10.31.100.2:8081` + +--- + +## 📄 第二步:修改 `yunzer.com.cn` 和 `dh2.fun` 的 Nginx 配置 + +为了绕过宝塔面板对 SSL 注释规则的保存验证限制,以下配置中**完全保留了宝塔原生生成的 `#SSL-START`、`#error_page 404/404.html;` 以及 `#SSL-END` 结构**。 + +请分别在宝塔面板对应站点的 **“配置文件”** 选项中,替换为以下内容: + +### 1. `yunzer.com.cn` 的 Nginx 配置文件 + +```nginx +server { + listen 80; + listen 443 ssl; + listen 443 quic; + http2 on; + + # 匹配主域名和所有租户的子域名 + server_name yunzer.com.cn ~^(?.+)\.yunzer\.com\.cn$; + index index.html; + + # 核心修改:直接指向部署了 frontend 的静态资源路径 + root /www/wwwroot/www.yunzer.cn; + + #CERT-APPLY-CHECK--START + include /www/server/panel/vhost/nginx/well-known/yunzer.com.cn.conf; + #CERT-APPLY-CHECK--END + include /www/server/panel/vhost/nginx/extension/yunzer.com.cn/*.conf; + + #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则 + #error_page 404/404.html; + + #HTTP_TO_HTTPS_START + set $isRedcert 1; + if ($server_port != 443) { + set $isRedcert 2; + } + if ( $uri ~ /\.well-known/ ) { + set $isRedcert 1; + } + if ($isRedcert != 1) { + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END + + ssl_certificate /www/server/panel/vhost/cert/yunzer.com.cn/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/yunzer.com.cn/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_tickets on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000"; + add_header Alt-Svc 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"'; + error_page 497 https://$host$request_uri; + #SSL-END + + #ERROR-PAGE-START 错误页配置,可以注释、删除或修改 + error_page 404 /404.html; + #ERROR-PAGE-END + + # --- 安全与防盗链拦截 START --- + location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ { + return 404; + } + + location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ { + return 404; + } + + location ~ \.well-known { + allow all; + } + + if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { + return 403; + } + # --- 安全与防盗链拦截 END --- + + # 静态资源缓存 + location ~* \.(gif|jpg|jpeg|png|bmp|swf)$ { + expires 30d; + error_log /dev/null; + access_log /dev/null; + } + + location ~* \.(js|css)$ { + expires 12h; + error_log /dev/null; + access_log /dev/null; + } + + # --- Go API 反向代理及 Vue 路由支持 --- + # 1. 代理 /api/ 请求到本地运行在 8081 端口的 Go 服务 + location /api/ { + proxy_pass http://10.31.100.2:8081/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 2. 代理文件上传目录到 Go 静态服务 + location /uploads/ { + proxy_pass http://10.31.100.2:8081/uploads/; + } + + # 3. 前端单页面应用 (Vue Router) History 模式重定向 + location / { + try_files $uri $uri/ /index.html; + } + + access_log /www/wwwlogs/yunzer.com.cn.log; + error_log /www/wwwlogs/yunzer.com.cn.error.log; +} +``` + +### 2. `dh2.fun` 的 Nginx 配置文件 + +```nginx +server { + listen 80; + listen 443 ssl; + listen 443 quic; + http2 on; + + # 匹配主域名和所有租户的子域名 + server_name dh2.fun ~^(?.+)\.dh2\.fun$; + index index.html; + + # 核心修改:直接指向部署了 frontend 的静态资源路径 + root /www/wwwroot/www.yunzer.cn; + + #CERT-APPLY-CHECK--START + include /www/server/panel/vhost/nginx/well-known/dh2.fun.conf; + #CERT-APPLY-CHECK--END + include /www/server/panel/vhost/nginx/extension/dh2.fun/*.conf; + + #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则 + #error_page 404/404.html; + + #HTTP_TO_HTTPS_START + set $isRedcert 1; + if ($server_port != 443) { + set $isRedcert 2; + } + if ( $uri ~ /\.well-known/ ) { + set $isRedcert 1; + } + if ($isRedcert != 1) { + rewrite ^(/.*)$ https://$host$1 permanent; + } + #HTTP_TO_HTTPS_END + + ssl_certificate /www/server/panel/vhost/cert/dh2.fun/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/dh2.fun/privkey.pem; + ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_tickets on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + add_header Strict-Transport-Security "max-age=31536000"; + add_header Alt-Svc 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"'; + error_page 497 https://$host$request_uri; + #SSL-END + + #ERROR-PAGE-START 错误页配置,可以注释、删除或修改 + error_page 404 /404.html; + #ERROR-PAGE-END + + # 禁止访问的敏感文件 + location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ + { + return 404; + } + + # 禁止访问的敏感目录 + location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ { + return 404; + } + + #一键申请SSL证书验证目录相关设置 + location ~ \.well-known{ + allow all; + } + + #禁止在证书验证目录放入敏感文件 + if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { + return 403; + } + + location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ + { + expires 30d; + error_log /dev/null; + access_log /dev/null; + } + + location ~ .*\.(js|css)?$ + { + expires 12h; + error_log /dev/null; + access_log /dev/null; + } + + # --- Go API 反向代理及 Vue 路由支持 --- + location /api/ { + proxy_pass http://10.31.100.2:8081/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /uploads/ { + proxy_pass http://10.31.100.2:8081/uploads/; + } + + location / { + try_files $uri $uri/ /index.html; + } + } +``` diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts index 22c6c8b..6efa39a 100644 --- a/frontend/src/router/routes.ts +++ b/frontend/src/router/routes.ts @@ -4,7 +4,7 @@ import { createComponentLoader } from '@/utils/pathResolver' export const constantRoute = [ { path: '/', - component: () => import('@/views/theme/index.vue'), + component: () => import('@/views/home/index.vue'), name: '首页', meta: { title: '首页', diff --git a/go/controllers/index_portal.go b/go/controllers/index_portal.go new file mode 100644 index 0000000..3b78796 --- /dev/null +++ b/go/controllers/index_portal.go @@ -0,0 +1,396 @@ +package controllers + +import ( + "strconv" + "strings" + + "server/models" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" +) + +// IndexPortalController 前台门户及租户官网公共接口(无需登录) +type IndexPortalController struct { + beego.Controller +} + +// getTidByHost 根据访问域名识别租户 ID,默认返回 1 +func (c *IndexPortalController) getTidByHost() uint64 { + host := c.Ctx.Request.Host + parts := strings.Split(host, ":") + domain := parts[0] + + if domain == "localhost" || domain == "127.0.0.1" { + return 1 // 开发环境默认租户 + } + + var td models.SystemTenantDomain + err := models.Orm.QueryTable(new(models.SystemTenantDomain)). + Filter("full_domain", domain). + Filter("status", 1). + Filter("delete_time__isnull", true). + One(&td) + if err == nil && td.Tid != nil { + return *td.Tid + } + return 1 // 默认租户 +} + +// GetHeadMenu GET /index/headmenu +func (c *IndexPortalController) GetHeadMenu() { + // 前台的静态导航菜单结构 + menuData := []map[string]interface{}{ + { + "id": 1, + "title": "首页", + "path": "/", + "type": 2, + }, + { + "id": 2, + "title": "解决方案", + "path": "/solutions", + "type": 2, + "children": []map[string]interface{}{ + { + "id": 21, + "title": "行业解决方案", + "path": "/solutions/solution", + "type": 2, + }, + { + "id": 22, + "title": "成功案例", + "path": "/solutions/successCases", + "type": 2, + }, + }, + }, + { + "id": 3, + "title": "新闻中心", + "path": "/newsCenter", + "type": 2, + "children": []map[string]interface{}{ + { + "id": 31, + "title": "站点新闻", + "path": "/newsCenter/news", + "type": 2, + }, + { + "id": 32, + "title": "站点公告", + "path": "/newsCenter/announcement", + "type": 2, + }, + { + "id": 33, + "title": "技术中心", + "path": "/newsCenter/technologyCenter", + "type": 2, + }, + }, + }, + } + + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": menuData, + } + _ = c.ServeJSON() +} + +// GetFooterData GET /index/footerdata +func (c *IndexPortalController) GetFooterData() { + tid := c.getTidByHost() + + // 准备默认的 Footer 配置数据 + footerMap := map[string]string{ + "sitename": "云泽官方网站", + "companyname": "云泽智能科技有限公司", + "copyright": "Copyright © 2026 Yunzer. All Rights Reserved.", + "icp": "苏ICP备2026000000号-1", + "description": "专业为企业建设数字化管理与官网门户的一站式 SaaS 服务平台", + "logo": "", + "logow": "", + "ico": "", + } + + // 尝试从租户站点设置表中查询最新的配置 + var rows []models.TenantSiteSetting + _, err := models.Orm.QueryTable(new(models.TenantSiteSetting)). + Filter("tid", tid). + Filter("delete_time__isnull", true). + Limit(1). + All(&rows) + if err == nil && len(rows) > 0 { + r := rows[0] + if r.Sitename != "" { + footerMap["sitename"] = r.Sitename + } + if r.Companyname != "" { + footerMap["companyname"] = r.Companyname + } + if r.Copyright != "" { + footerMap["copyright"] = r.Copyright + } + if r.Icp != "" { + footerMap["icp"] = r.Icp + } + if r.Description != "" { + footerMap["description"] = r.Description + } + footerMap["logo"] = r.Logo + footerMap["logow"] = r.Logow + footerMap["ico"] = r.Ico + } + + // 将 Map 转换为前端需要的 [{label: "...", value: "..."}] 数组格式 + var dataList []map[string]string + for k, v := range footerMap { + dataList = append(dataList, map[string]string{ + "label": k, + "value": v, + }) + } + + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": dataList, + } + _ = c.ServeJSON() +} + +// GetNewsCenterTop4 GET /index/newscentertop4 +func (c *IndexPortalController) GetNewsCenterTop4() { + tid := c.getTidByHost() + _ = models.EnsureCmsArticleTables() + + var articles []models.CmsArticle + _, err := models.Orm.QueryTable(new(models.CmsArticle)). + Filter("tid", tid). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("-top", "-recommend", "-id"). + Limit(4). + All(&articles) + + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 200, "data": []interface{}{}} + _ = c.ServeJSON() + return + } + + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": articles, + } + _ = c.ServeJSON() +} + +// queryCmsArticles 统一的分页查询文章辅助方法 +func (c *IndexPortalController) queryCmsArticles(categoryName string, categoryID int64) { + tid := c.getTidByHost() + _ = models.EnsureCmsArticleTables() + + page, _ := c.GetInt("page", 1) + limit, _ := c.GetInt("limit", 10) + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 10 + } + + qs := models.Orm.QueryTable(new(models.CmsArticle)). + Filter("tid", tid). + Filter("status", 1). + Filter("delete_time__isnull", true) + + // 如果有传具体的分类 ID + if categoryID > 0 { + qs = qs.Filter("cate_id", categoryID) + } + + total, _ := qs.Count() + + var articles []models.CmsArticle + _, err := qs.OrderBy("-top", "-id"). + Limit(limit, (page-1)*limit). + All(&articles) + + if err != nil { + c.Data["json"] = map[string]interface{}{ + "code": 200, + "data": map[string]interface{}{ + "list": []interface{}{}, + "total": 0, + }, + } + _ = c.ServeJSON() + return + } + + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": map[string]interface{}{ + "list": articles, + "total": total, + }, + } + _ = c.ServeJSON() +} + +// GetCompanyNews GET /index/companynews +func (c *IndexPortalController) GetCompanyNews() { + // 可根据实际需要分配固定的分类 ID + c.queryCmsArticles("companynews", 0) +} + +// GetKingdeeNews GET /index/kingdeenews +func (c *IndexPortalController) GetKingdeeNews() { + c.queryCmsArticles("kingdeenews", 0) +} + +// GetTechnologyCenter GET /index/technologyCenter +func (c *IndexPortalController) GetTechnologyCenter() { + categoryID, _ := c.GetInt64("category_id", 0) + c.queryCmsArticles("technologyCenter", categoryID) +} + +// GetTechnologyCategories GET /index/technologyCategories +func (c *IndexPortalController) GetTechnologyCategories() { + tid := c.getTidByHost() + _ = models.EnsureCmsArticleTables() + + var categories []models.CmsArticleCategory + _, err := models.Orm.QueryTable(new(models.CmsArticleCategory)). + Filter("tid", tid). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("sort", "id"). + All(&categories) + + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 200, "data": []interface{}{}} + _ = c.ServeJSON() + return + } + + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": categories, + } + _ = c.ServeJSON() +} + +// GetCompanyNewsDetail GET /index/companynews/detail/:id +func (c *IndexPortalController) GetCompanyNewsDetail() { + c.getArticleDetail() +} + +// GetKingdeeNewsDetail GET /index/kingdeenews/detail/:id +func (c *IndexPortalController) GetKingdeeNewsDetail() { + c.getArticleDetail() +} + +// getArticleDetail 查询文章详情的内部辅助方法 +func (c *IndexPortalController) getArticleDetail() { + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + tid := c.getTidByHost() + _ = models.EnsureCmsArticleTables() + + var article models.CmsArticle + err := models.Orm.QueryTable(new(models.CmsArticle)). + Filter("id", id). + Filter("tid", tid). + Filter("delete_time__isnull", true). + One(&article) + + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 404, "msg": "文章不存在"} + _ = c.ServeJSON() + return + } + + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": article, + } + _ = c.ServeJSON() +} + +// AddLike POST /index/articleLikes/:id +func (c *IndexPortalController) AddLike() { + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + tid := c.getTidByHost() + _ = models.EnsureCmsArticleTables() + + _, _ = models.Orm.QueryTable(new(models.CmsArticle)). + Filter("id", id). + Filter("tid", tid). + Update(orm.Params{ + "likes": orm.ColValue(orm.ColAdd, 1), + }) + + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "点赞成功"} + _ = c.ServeJSON() +} + +// ReduceLike POST /index/articleUnlikes/:id +func (c *IndexPortalController) ReduceLike() { + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + tid := c.getTidByHost() + _ = models.EnsureCmsArticleTables() + + _, _ = models.Orm.QueryTable(new(models.CmsArticle)). + Filter("id", id). + Filter("tid", tid). + Filter("likes__gt", 0). + Update(orm.Params{ + "likes": orm.ColValue(orm.ColMinus, 1), + }) + + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "已取消点赞"} + _ = c.ServeJSON() +} + +// AddViews POST /index/views/:id +func (c *IndexPortalController) AddViews() { + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + tid := c.getTidByHost() + _ = models.EnsureCmsArticleTables() + + _, _ = models.Orm.QueryTable(new(models.CmsArticle)). + Filter("id", id). + Filter("tid", tid). + Update(orm.Params{ + "views": orm.ColValue(orm.ColAdd, 1), + }) + + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "阅读量+1"} + _ = c.ServeJSON() +} + +// GetOnePageByPath GET /index/onepage/:path +func (c *IndexPortalController) GetOnePageByPath() { + // 简单返回空数据,防止前端单页接口报错 + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "success", + "data": nil, + } + _ = c.ServeJSON() +} diff --git a/go/controllers/platform_account_pool.go b/go/controllers/platform_account_pool.go index c1514dd..541534c 100644 --- a/go/controllers/platform_account_pool.go +++ b/go/controllers/platform_account_pool.go @@ -401,41 +401,168 @@ func batchAddPoolRows(c *beego.Controller, module string) { return } } + var accounts []string + var tokens []string + seenAccs := make(map[string]bool) + seenTks := make(map[string]bool) + for _, row := range payload.Rows { + acc := strings.TrimSpace(row.Account) + tk := strings.TrimSpace(row.Token) + if acc != "" && !seenAccs[acc] { + seenAccs[acc] = true + accounts = append(accounts, acc) + } + if tk != "" && !seenTks[tk] { + seenTks[tk] = true + tokens = append(tokens, tk) + } + } + + existingAccs := make(map[string]bool) + existingTks := make(map[string]bool) + + if len(accounts) > 0 || len(tokens) > 0 { + condObj := orm.NewCondition() + subCond := orm.NewCondition() + if len(accounts) > 0 { + subCond = subCond.Or("account__in", accounts) + } + if len(tokens) > 0 { + subCond = subCond.Or("token__in", tokens) + } + + switch module { + case "cursor": + var dbRows []models.PlatformAccountPoolCursor + cond := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("delete_time__isnull", true) + cond = cond.SetCond(condObj.AndCond(subCond)) + if _, err := cond.All(&dbRows, "account", "token"); err == nil { + for _, r := range dbRows { + if r.Account != "" { + existingAccs[r.Account] = true + } + if r.Token != "" { + existingTks[r.Token] = true + } + } + } + case "windsurf": + var dbRows []models.PlatformAccountPoolWindsurf + cond := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).Filter("delete_time__isnull", true) + cond = cond.SetCond(condObj.AndCond(subCond)) + if _, err := cond.All(&dbRows, "account", "token"); err == nil { + for _, r := range dbRows { + if r.Account != "" { + existingAccs[r.Account] = true + } + if r.Token != "" { + existingTks[r.Token] = true + } + } + } + case "krio": + var dbRows []models.PlatformAccountPoolKiro + cond := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).Filter("delete_time__isnull", true) + cond = cond.SetCond(condObj.AndCond(subCond)) + if _, err := cond.All(&dbRows, "account", "token"); err == nil { + for _, r := range dbRows { + if r.Account != "" { + existingAccs[r.Account] = true + } + if r.Token != "" { + existingTks[r.Token] = true + } + } + } + case "codex": + var dbRows []models.PlatformAccountPoolCodex + cond := models.Orm.QueryTable(new(models.PlatformAccountPoolCodex)).Filter("delete_time__isnull", true) + cond = cond.SetCond(condObj.AndCond(subCond)) + if _, err := cond.All(&dbRows, "account", "token"); err == nil { + for _, r := range dbRows { + if r.Account != "" { + existingAccs[r.Account] = true + } + if r.Token != "" { + existingTks[r.Token] = true + } + } + } + } + } + + insertedAccs := make(map[string]bool) + insertedTks := make(map[string]bool) + + var successCount int + var duplicateCount int + + for _, row := range payload.Rows { + acc := strings.TrimSpace(row.Account) + tk := strings.TrimSpace(row.Token) + + isDuplicate := false + if row.DataType == "account" { + if existingAccs[acc] || insertedAccs[acc] { + isDuplicate = true + } + } else if row.DataType == "tk" { + if existingTks[tk] || insertedTks[tk] { + isDuplicate = true + } + } else if row.DataType == "account_tk" { + if (acc != "" && (existingAccs[acc] || insertedAccs[acc])) || (tk != "" && (existingTks[tk] || insertedTks[tk])) { + isDuplicate = true + } + } + + if isDuplicate { + duplicateCount++ + continue + } + + if acc != "" { + insertedAccs[acc] = true + } + if tk != "" { + insertedTks[tk] = true + } + switch module { case "cursor": _, err = models.Orm.Insert(&models.PlatformAccountPoolCursor{ DataType: strings.TrimSpace(row.DataType), - Account: strings.TrimSpace(row.Account), + Account: acc, Password: strings.TrimSpace(row.Password), - Token: strings.TrimSpace(row.Token), + Token: tk, Remark: strings.TrimSpace(row.Remark), IsExtracted: 0, }) case "windsurf": _, err = models.Orm.Insert(&models.PlatformAccountPoolWindsurf{ DataType: strings.TrimSpace(row.DataType), - Account: strings.TrimSpace(row.Account), + Account: acc, Password: strings.TrimSpace(row.Password), - Token: strings.TrimSpace(row.Token), + Token: tk, Remark: strings.TrimSpace(row.Remark), IsExtracted: 0, }) case "krio": _, err = models.Orm.Insert(&models.PlatformAccountPoolKiro{ DataType: strings.TrimSpace(row.DataType), - Account: strings.TrimSpace(row.Account), + Account: acc, Password: strings.TrimSpace(row.Password), - Token: strings.TrimSpace(row.Token), + Token: tk, Remark: strings.TrimSpace(row.Remark), IsExtracted: 0, }) case "codex": _, err = models.Orm.Insert(&models.PlatformAccountPoolCodex{ DataType: strings.TrimSpace(row.DataType), - Account: strings.TrimSpace(row.Account), + Account: acc, Password: strings.TrimSpace(row.Password), - Token: strings.TrimSpace(row.Token), + Token: tk, Remark: strings.TrimSpace(row.Remark), IsExtracted: 0, }) @@ -447,8 +574,16 @@ func batchAddPoolRows(c *beego.Controller, module string) { poolJSONErr(c, 500, 500, "批量添加失败: "+err.Error()) return } + successCount++ + } + c.Data["json"] = map[string]interface{}{ + "code": 200, + "msg": "批量添加成功", + "data": map[string]interface{}{ + "success_count": successCount, + "duplicate_count": duplicateCount, + }, } - c.Data["json"] = map[string]interface{}{"code": 200, "msg": "批量添加成功"} _ = c.ServeJSON() } diff --git a/go/routers/index/index.go b/go/routers/index/index.go index c2cd5d3..fc3b2d2 100644 --- a/go/routers/index/index.go +++ b/go/routers/index/index.go @@ -1,7 +1,24 @@ -package index - -// Register 注册前端站点(index)路由。 -// 建议统一使用 /index/* 或根路径,根据后续设计补充。 -func Register() { -} - +package index + +import ( + "server/controllers" + + beego "github.com/beego/beego/v2/server/web" +) + +// Register 注册前端站点(index)路由。 +func Register() { + beego.Router("/index/headmenu", &controllers.IndexPortalController{}, "get:GetHeadMenu") + beego.Router("/index/footerdata", &controllers.IndexPortalController{}, "get:GetFooterData") + beego.Router("/index/newscentertop4", &controllers.IndexPortalController{}, "get:GetNewsCenterTop4") + beego.Router("/index/companynews", &controllers.IndexPortalController{}, "get:GetCompanyNews") + beego.Router("/index/kingdeenews", &controllers.IndexPortalController{}, "get:GetKingdeeNews") + beego.Router("/index/technologyCenter", &controllers.IndexPortalController{}, "get:GetTechnologyCenter") + beego.Router("/index/technologyCategories", &controllers.IndexPortalController{}, "get:GetTechnologyCategories") + beego.Router("/index/companynews/detail/:id", &controllers.IndexPortalController{}, "get:GetCompanyNewsDetail") + beego.Router("/index/kingdeenews/detail/:id", &controllers.IndexPortalController{}, "get:GetKingdeeNewsDetail") + beego.Router("/index/articleLikes/:id", &controllers.IndexPortalController{}, "post:AddLike") + beego.Router("/index/articleUnlikes/:id", &controllers.IndexPortalController{}, "post:ReduceLike") + beego.Router("/index/views/:id", &controllers.IndexPortalController{}, "post:AddViews") + beego.Router("/index/onepage/:path", &controllers.IndexPortalController{}, "get:GetOnePageByPath") +} diff --git a/platform/src/views/accountpool/codex/index.vue b/platform/src/views/accountpool/codex/index.vue index 11a827e..384033e 100644 --- a/platform/src/views/accountpool/codex/index.vue +++ b/platform/src/views/accountpool/codex/index.vue @@ -103,19 +103,24 @@ function openAddDialog(mode = "single") { async function saveRows(rows) { if (!rows.length) return; if (rows.length === 1) { - await addAccountPool(moduleKey, rows[0]); - return; + return await addAccountPool(moduleKey, rows[0]); } - await batchAddAccountPool(moduleKey, rows); + return await batchAddAccountPool(moduleKey, rows); } async function handleEditSubmit(payload) { loading.value = true; try { - await saveRows(payload.rows || []); - ElMessage.success( - payload.mode === "batch" ? "批量添加成功" : "账号添加成功", - ); + const res = await saveRows(payload.rows || []); + if (payload.mode === "batch") { + const successCount = res?.data?.success_count ?? 0; + const duplicateCount = res?.data?.duplicate_count ?? 0; + ElMessage.success( + `批量添加完成:成功 ${successCount} 条,重复跳过 ${duplicateCount} 条`, + ); + } else { + ElMessage.success("账号添加成功"); + } await fetchList(); } finally { loading.value = false; diff --git a/platform/src/views/accountpool/cursor/index.vue b/platform/src/views/accountpool/cursor/index.vue index c30d488..b05119a 100644 --- a/platform/src/views/accountpool/cursor/index.vue +++ b/platform/src/views/accountpool/cursor/index.vue @@ -165,19 +165,24 @@ function nowText() { async function saveRows(rows) { if (!rows.length) return; if (rows.length === 1) { - await addAccountPool(moduleKey, rows[0]); - return; + return await addAccountPool(moduleKey, rows[0]); } - await batchAddAccountPool(moduleKey, rows); + return await batchAddAccountPool(moduleKey, rows); } async function handleEditSubmit(payload) { loading.value = true; try { - await saveRows(payload.rows || []); - ElMessage.success( - payload.mode === "batch" ? "批量添加成功" : "账号添加成功", - ); + const res = await saveRows(payload.rows || []); + if (payload.mode === "batch") { + const successCount = res?.data?.success_count ?? 0; + const duplicateCount = res?.data?.duplicate_count ?? 0; + ElMessage.success( + `批量添加完成:成功 ${successCount} 条,重复跳过 ${duplicateCount} 条`, + ); + } else { + ElMessage.success("账号添加成功"); + } await fetchList(); } finally { loading.value = false; diff --git a/platform/src/views/accountpool/kiro/index.vue b/platform/src/views/accountpool/kiro/index.vue index 935e3ab..7d0d499 100644 --- a/platform/src/views/accountpool/kiro/index.vue +++ b/platform/src/views/accountpool/kiro/index.vue @@ -103,19 +103,24 @@ function openAddDialog(mode = "single") { async function saveRows(rows) { if (!rows.length) return; if (rows.length === 1) { - await addAccountPool(moduleKey, rows[0]); - return; + return await addAccountPool(moduleKey, rows[0]); } - await batchAddAccountPool(moduleKey, rows); + return await batchAddAccountPool(moduleKey, rows); } async function handleEditSubmit(payload) { loading.value = true; try { - await saveRows(payload.rows || []); - ElMessage.success( - payload.mode === "batch" ? "批量添加成功" : "账号添加成功", - ); + const res = await saveRows(payload.rows || []); + if (payload.mode === "batch") { + const successCount = res?.data?.success_count ?? 0; + const duplicateCount = res?.data?.duplicate_count ?? 0; + ElMessage.success( + `批量添加完成:成功 ${successCount} 条,重复跳过 ${duplicateCount} 条`, + ); + } else { + ElMessage.success("账号添加成功"); + } await fetchList(); } finally { loading.value = false; diff --git a/platform/src/views/accountpool/windsurf/index.vue b/platform/src/views/accountpool/windsurf/index.vue index 86529c9..3233ee3 100644 --- a/platform/src/views/accountpool/windsurf/index.vue +++ b/platform/src/views/accountpool/windsurf/index.vue @@ -89,19 +89,24 @@ function openAddDialog(mode = "single") { async function saveRows(rows) { if (!rows.length) return; if (rows.length === 1) { - await addAccountPool(moduleKey, rows[0]); - return; + return await addAccountPool(moduleKey, rows[0]); } - await batchAddAccountPool(moduleKey, rows); + return await batchAddAccountPool(moduleKey, rows); } async function handleEditSubmit(payload) { loading.value = true; try { - await saveRows(payload.rows || []); - ElMessage.success( - payload.mode === "batch" ? "批量添加成功" : "账号添加成功", - ); + const res = await saveRows(payload.rows || []); + if (payload.mode === "batch") { + const successCount = res?.data?.success_count ?? 0; + const duplicateCount = res?.data?.duplicate_count ?? 0; + ElMessage.success( + `批量添加完成:成功 ${successCount} 条,重复跳过 ${duplicateCount} 条`, + ); + } else { + ElMessage.success("账号添加成功"); + } await fetchList(); } finally { loading.value = false;