diff --git a/.kiro/specs/content-auxiliary-frontend/design.md b/.kiro/specs/content-auxiliary-frontend/design.md new file mode 100644 index 00000000..e22a4ad5 --- /dev/null +++ b/.kiro/specs/content-auxiliary-frontend/design.md @@ -0,0 +1,572 @@ +# Design Document + +## Overview + +本设计文档描述"内容与辅助"模块从老项目(PHP ThinkPHP + Layui)迁移到新项目(ASP.NET Core + Vue 3 + Element Plus)的技术设计方案。该模块包含三个子模块:单页管理(Danye)、悬浮球配置(FloatBall)、福利屋入口(WelfareHouse)。 + +## Architecture + +### 系统架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 前端 (Vue 3 + Element Plus) │ +├─────────────────────────────────────────────────────────────────┤ +│ views/business/ │ +│ ├── danye/ # 单页管理页面 │ +│ │ ├── list.vue # 单页列表 │ +│ │ └── components/ # 组件 │ +│ ├── floatball/ # 悬浮球配置页面 │ +│ │ ├── list.vue # 悬浮球列表 │ +│ │ └── components/ # 组件 │ +│ └── welfarehouse/ # 福利屋入口页面 │ +│ ├── list.vue # 福利屋列表 │ +│ └── components/ # 组件 │ +├─────────────────────────────────────────────────────────────────┤ +│ api/business/ │ +│ ├── danye.ts # 单页API │ +│ ├── floatball.ts # 悬浮球API │ +│ └── welfarehouse.ts # 福利屋API │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 后端 (ASP.NET Core) │ +├─────────────────────────────────────────────────────────────────┤ +│ HoneyBox.Admin.Business/ │ +│ ├── Controllers/ │ +│ │ ├── DanyeController.cs │ +│ │ ├── FloatBallController.cs │ +│ │ └── WelfareHouseController.cs │ +│ ├── Services/ │ +│ │ ├── DanyeService.cs │ +│ │ ├── FloatBallService.cs │ +│ │ └── WelfareHouseService.cs │ +│ └── Models/ │ +│ ├── Danye/ │ +│ ├── FloatBall/ │ +│ └── WelfareHouse/ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 数据库 (SQL Server) │ +├─────────────────────────────────────────────────────────────────┤ +│ ├── danye # 单页内容表 (已存在) │ +│ ├── float_ball_config # 悬浮球配置表 (已存在) │ +│ └── welfare_house # 福利屋配置表 (已存在) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 数据库实体(已存在) + +#### Danye 实体 +```csharp +public class Danye +{ + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public int UpdateTime { get; set; } + public byte Status { get; set; } + public byte IsImageOptimizer { get; set; } +} +``` + +#### FloatBallConfig 实体 +```csharp +public class FloatBallConfig +{ + public int Id { get; set; } + public byte Status { get; set; } + public byte Type { get; set; } // 1展示图片 2跳转页面 + public string Image { get; set; } + public string LinkUrl { get; set; } + public string PositionX { get; set; } + public string PositionY { get; set; } + public string Width { get; set; } + public string Height { get; set; } + public byte Effect { get; set; } // 0无 1缩放动画 + public string? Title { get; set; } + public string? ImageDetails { get; set; } + public string? ImageBj { get; set; } + public string? ImageDetailsX { get; set; } + public string? ImageDetailsY { get; set; } + public string? ImageDetailsW { get; set; } + public string? ImageDetailsH { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} +``` + +#### WelfareHouse 实体 +```csharp +public class WelfareHouse +{ + public int Id { get; set; } + public string Name { get; set; } + public string Image { get; set; } + public string Url { get; set; } + public int Sort { get; set; } + public byte Status { get; set; } + public int? CreateTime { get; set; } + public int? UpdateTime { get; set; } +} +``` + +## Components + +### 后端组件 + +#### 1. DanyeController +- `GET /api/business/danye` - 获取单页列表 +- `GET /api/business/danye/{id}` - 获取单页详情 +- `PUT /api/business/danye/{id}` - 更新单页内容 +- `PUT /api/business/danye/{id}/image-optimizer` - 切换图片优化状态 + +#### 2. FloatBallController +- `GET /api/business/floatball` - 获取悬浮球列表(分页) +- `GET /api/business/floatball/{id}` - 获取悬浮球详情 +- `POST /api/business/floatball` - 新增悬浮球 +- `PUT /api/business/floatball/{id}` - 更新悬浮球 +- `DELETE /api/business/floatball/{id}` - 删除悬浮球 +- `PUT /api/business/floatball/{id}/status` - 切换状态 + +#### 3. WelfareHouseController +- `GET /api/business/welfarehouse` - 获取福利屋列表(分页) +- `GET /api/business/welfarehouse/{id}` - 获取福利屋详情 +- `POST /api/business/welfarehouse` - 新增福利屋入口 +- `PUT /api/business/welfarehouse/{id}` - 更新福利屋入口 +- `DELETE /api/business/welfarehouse/{id}` - 删除福利屋入口 +- `PUT /api/business/welfarehouse/{id}/status` - 切换状态 + +### 前端组件 + +#### 1. 单页管理模块 +- `DanyeList.vue` - 单页列表页面 +- `DanyeTable.vue` - 单页表格组件 +- `DanyeFormDialog.vue` - 单页编辑弹窗(含富文本编辑器) + +#### 2. 悬浮球配置模块 +- `FloatBallList.vue` - 悬浮球列表页面 +- `FloatBallTable.vue` - 悬浮球表格组件 +- `FloatBallFormDialog.vue` - 悬浮球表单弹窗 + +#### 3. 福利屋入口模块 +- `WelfareHouseList.vue` - 福利屋列表页面 +- `WelfareHouseTable.vue` - 福利屋表格组件 +- `WelfareHouseFormDialog.vue` - 福利屋表单弹窗 + +## Data Models + +### 后端 Models + +#### Danye Models +```csharp +// 列表响应 +public class DanyeResponse +{ + public int Id { get; set; } + public string Title { get; set; } + public bool IsImageOptimizer { get; set; } + public DateTime UpdateTime { get; set; } +} + +// 详情响应 +public class DanyeDetailResponse +{ + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public bool IsImageOptimizer { get; set; } +} + +// 更新请求 +public class DanyeUpdateRequest +{ + public string? Title { get; set; } + public string Content { get; set; } +} +``` + +#### FloatBall Models +```csharp +// 列表请求 +public class FloatBallListRequest +{ + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; +} + +// 响应 +public class FloatBallResponse +{ + public int Id { get; set; } + public string? Title { get; set; } + public int Type { get; set; } + public string Image { get; set; } + public string? ImageBj { get; set; } + public string? ImageDetails { get; set; } + public string LinkUrl { get; set; } + public string PositionX { get; set; } + public string PositionY { get; set; } + public string Width { get; set; } + public string Height { get; set; } + public string? ImageDetailsX { get; set; } + public string? ImageDetailsY { get; set; } + public string? ImageDetailsW { get; set; } + public string? ImageDetailsH { get; set; } + public int Effect { get; set; } + public int Status { get; set; } + public DateTime CreatedAt { get; set; } +} + +// 创建/更新请求 +public class FloatBallCreateRequest +{ + public string? Title { get; set; } + public int Type { get; set; } + public string Image { get; set; } + public string? ImageBj { get; set; } + public string? ImageDetails { get; set; } + public string? LinkUrl { get; set; } + public string PositionX { get; set; } + public string PositionY { get; set; } + public string Width { get; set; } + public string Height { get; set; } + public string? ImageDetailsX { get; set; } + public string? ImageDetailsY { get; set; } + public string? ImageDetailsW { get; set; } + public string? ImageDetailsH { get; set; } + public int Effect { get; set; } + public int Status { get; set; } = 1; +} +``` + +#### WelfareHouse Models +```csharp +// 列表请求 +public class WelfareHouseListRequest +{ + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; +} + +// 响应 +public class WelfareHouseResponse +{ + public int Id { get; set; } + public string Name { get; set; } + public string Image { get; set; } + public string Url { get; set; } + public int Sort { get; set; } + public int Status { get; set; } + public DateTime? CreateTime { get; set; } +} + +// 创建/更新请求 +public class WelfareHouseCreateRequest +{ + public string Name { get; set; } + public string Image { get; set; } + public string Url { get; set; } + public int Sort { get; set; } + public int Status { get; set; } = 1; +} +``` + +### 前端 TypeScript 接口 + +```typescript +// Danye +interface DanyeResponse { + id: number + title: string + isImageOptimizer: boolean + updateTime: string +} + +interface DanyeDetailResponse { + id: number + title: string + content: string + isImageOptimizer: boolean +} + +interface DanyeUpdateRequest { + title?: string + content: string +} + +// FloatBall +interface FloatBallListRequest { + page: number + pageSize: number +} + +interface FloatBallResponse { + id: number + title?: string + type: number + image: string + imageBj?: string + imageDetails?: string + linkUrl: string + positionX: string + positionY: string + width: string + height: string + imageDetailsX?: string + imageDetailsY?: string + imageDetailsW?: string + imageDetailsH?: string + effect: number + status: number + createdAt: string +} + +interface FloatBallCreateRequest { + title?: string + type: number + image: string + imageBj?: string + imageDetails?: string + linkUrl?: string + positionX: string + positionY: string + width: string + height: string + imageDetailsX?: string + imageDetailsY?: string + imageDetailsW?: string + imageDetailsH?: string + effect: number + status?: number +} + +// WelfareHouse +interface WelfareHouseListRequest { + page: number + pageSize: number +} + +interface WelfareHouseResponse { + id: number + name: string + image: string + url: string + sort: number + status: number + createTime?: string +} + +interface WelfareHouseCreateRequest { + name: string + image: string + url: string + sort: number + status?: number +} +``` + +## API Design + +### 单页管理 API + +| 方法 | 路径 | 描述 | 请求体 | 响应 | +|------|------|------|--------|------| +| GET | /api/business/danye | 获取单页列表 | - | `ApiResponse>` | +| GET | /api/business/danye/{id} | 获取单页详情 | - | `ApiResponse` | +| PUT | /api/business/danye/{id} | 更新单页内容 | `DanyeUpdateRequest` | `ApiResponse` | +| PUT | /api/business/danye/{id}/image-optimizer | 切换图片优化 | `{ isImageOptimizer: bool }` | `ApiResponse` | + +### 悬浮球配置 API + +| 方法 | 路径 | 描述 | 请求体 | 响应 | +|------|------|------|--------|------| +| GET | /api/business/floatball | 获取悬浮球列表 | Query: page, pageSize | `PagedResponse` | +| GET | /api/business/floatball/{id} | 获取悬浮球详情 | - | `ApiResponse` | +| POST | /api/business/floatball | 新增悬浮球 | `FloatBallCreateRequest` | `ApiResponse` | +| PUT | /api/business/floatball/{id} | 更新悬浮球 | `FloatBallCreateRequest` | `ApiResponse` | +| DELETE | /api/business/floatball/{id} | 删除悬浮球 | - | `ApiResponse` | +| PUT | /api/business/floatball/{id}/status | 切换状态 | `{ status: int }` | `ApiResponse` | + +### 福利屋入口 API + +| 方法 | 路径 | 描述 | 请求体 | 响应 | +|------|------|------|--------|------| +| GET | /api/business/welfarehouse | 获取福利屋列表 | Query: page, pageSize | `PagedResponse` | +| GET | /api/business/welfarehouse/{id} | 获取福利屋详情 | - | `ApiResponse` | +| POST | /api/business/welfarehouse | 新增福利屋入口 | `WelfareHouseCreateRequest` | `ApiResponse` | +| PUT | /api/business/welfarehouse/{id} | 更新福利屋入口 | `WelfareHouseCreateRequest` | `ApiResponse` | +| DELETE | /api/business/welfarehouse/{id} | 删除福利屋入口 | - | `ApiResponse` | +| PUT | /api/business/welfarehouse/{id}/status | 切换状态 | `{ status: int }` | `ApiResponse` | + +## UI Design + +### 单页管理页面 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 单页管理 │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ ID │ 标题 │ 图片优化 │ 更新时间 │ 操作 │ │ +│ ├────┼────────────────────────┼─────────┼───────────┼───────┤ │ +│ │ 1 │ 关于我们 │ [开关] │ 2026-01-18│ [编辑]│ │ +│ │ 2 │ 用户协议 │ [开关] │ 2026-01-18│ [编辑]│ │ +│ │ 3 │ 隐私政策 │ [开关] │ 2026-01-18│ [编辑]│ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 单页编辑弹窗 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 编辑单页 [X] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 标题: [关于我们________________] (ID 2-20 不可编辑) │ +│ │ +│ 内容: │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ [B] [I] [U] [图片] [链接] ... ││ +│ ├─────────────────────────────────────────────────────────────┤│ +│ │ ││ +│ │ 富文本编辑区域 ││ +│ │ ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ │ +│ [取消] [保存] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 悬浮球列表页面 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 悬浮球配置 [+ 新增] │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ ID │ 标题 │ 图片 │ 背景图 │ 详情图 │ 类型 │ 链接 │ 位置 │ ... │ │ +│ ├────┼─────┼─────┼───────┼───────┼─────┼─────┼─────┼─────┤ │ +│ │ 1 │ 活动 │ [img]│ [img] │ [img] │ 跳转 │ /act│ 10,20│ ... │ │ +│ │ 2 │ 客服 │ [img]│ - │ - │ 展示 │ - │ 10,80│ ... │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ 共 2 条 < 1 > │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 悬浮球表单弹窗 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 新增悬浮球 [X] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 标题: [____________________] │ +│ │ +│ 类型: (●) 展示图片 ( ) 跳转页面 │ +│ │ +│ 悬浮球图片: [上传] [预览] │ +│ 背景图片: [上传] [预览] │ +│ 详情图片: [上传] [预览] │ +│ │ +│ 跳转链接: [____________________] (类型为跳转时显示) │ +│ │ +│ 位置设置: │ +│ X: [____] Y: [____] │ +│ │ +│ 尺寸设置: │ +│ 宽: [____] 高: [____] │ +│ │ +│ 详情图位置: │ +│ X: [____] Y: [____] 宽: [____] 高: [____] │ +│ │ +│ 特效: [无特效 ▼] │ +│ 状态: [开关] │ +│ │ +│ [取消] [保存] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 福利屋入口列表页面 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 福利屋入口 [+ 新增] │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ ID │ 名称 │ 图片 │ 链接 │ 排序 │ 状态 │ 创建时间 │ 操作│ +│ ├────┼─────────┼──────┼──────────┼─────┼─────┼─────────┼─────┤ │ +│ │ 1 │ 签到有礼 │ [img] │ /sign │ 1 │ [开关]│ 01-18 │ [编辑][删除]│ +│ │ 2 │ 新人福利 │ [img] │ /newbie │ 2 │ [开关]│ 01-18 │ [编辑][删除]│ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ 共 2 条 < 1 > │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 福利屋入口表单弹窗 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 新增福利屋入口 [X] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 名称: [____________________] * │ +│ │ +│ 图片: [上传] [预览] * │ +│ │ +│ 跳转链接: [____________________] * │ +│ │ +│ 排序: [____] * │ +│ │ +│ 状态: [开关] │ +│ │ +│ [取消] [保存] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Security Considerations + +1. **权限控制**: 所有 API 需要验证管理员登录状态和权限 +2. **输入验证**: 后端对所有输入进行验证,防止 XSS 和 SQL 注入 +3. **富文本安全**: 富文本内容需要进行 HTML 净化处理 +4. **图片上传**: 验证图片类型和大小,防止恶意文件上传 + +## Performance Considerations + +1. **分页加载**: 悬浮球和福利屋列表使用分页加载 +2. **图片懒加载**: 表格中的图片使用懒加载 +3. **富文本编辑器**: 按需加载富文本编辑器组件 + +## Testing Strategy + +1. **单元测试**: 后端 Service 层单元测试 +2. **API 测试**: 使用 Swagger 测试 API 接口 +3. **前端组件测试**: Vue 组件单元测试 +4. **属性测试**: 验证搜索、分页、表单验证等通用属性 + +## Dependencies + +### 后端依赖 +- Entity Framework Core (已有) +- Mapster (已有) + +### 前端依赖 +- Element Plus (已有) +- WangEditor 或 TinyMCE (富文本编辑器,需确认项目中使用的编辑器) +- Vue Router (已有) + +## References + +- Requirements: `.kiro/specs/content-auxiliary-frontend/requirements.md` +- PHP 参考代码: `server/php/app/admin/controller/Danye.php` +- PHP 参考代码: `server/php/app/admin/controller/FloatBall.php` +- PHP 参考代码: `server/php/app/admin/controller/WelfareHouse.php` +- 实体定义: `server/HoneyBox/src/HoneyBox.Model/Entities/Danye.cs` +- 实体定义: `server/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs` +- 实体定义: `server/HoneyBox/src/HoneyBox.Model/Entities/WelfareHouse.cs` diff --git a/.kiro/specs/content-auxiliary-frontend/requirements.md b/.kiro/specs/content-auxiliary-frontend/requirements.md new file mode 100644 index 00000000..527ef173 --- /dev/null +++ b/.kiro/specs/content-auxiliary-frontend/requirements.md @@ -0,0 +1,146 @@ +# Requirements Document + +## Introduction + +本文档定义了"内容与辅助"模块从老项目(PHP ThinkPHP + Layui)迁移到新项目(ASP.NET Core + Vue 3 + Element Plus)的需求。该模块包含单页管理、悬浮球配置和福利屋入口三个子模块。 + +## Glossary + +- **Admin_System**: 后台管理系统 +- **Danye_Module**: 单页管理模块,用于管理静态页面内容(如关于我们、用户协议等) +- **FloatBall_Module**: 悬浮球配置模块,用于配置首页悬浮球 +- **WelfareHouse_Module**: 福利屋入口模块,用于配置福利屋入口 +- **Rich_Text_Editor**: 富文本编辑器,用于编辑HTML内容 +- **Image_Optimizer**: 图片优化开关,开启后只显示图片,支持长按识别二维码 + +## Requirements + +### Requirement 1: 单页管理列表 + +**User Story:** As an administrator, I want to view and manage static pages, so that I can maintain content like "About Us" and "User Agreement". + +#### Acceptance Criteria + +1. WHEN an administrator accesses the Danye list page, THE Admin_System SHALL display a list of all static pages with ID, title, and image optimizer status +2. WHEN an administrator clicks the edit button, THE Admin_System SHALL open an edit dialog with the page content +3. WHEN an administrator toggles the image optimizer switch, THE Admin_System SHALL update the page's image optimizer status immediately +4. THE Admin_System SHALL display the image optimizer status as a toggle switch for each page + +### Requirement 2: 单页编辑 + +**User Story:** As an administrator, I want to edit static page content, so that I can update the information displayed to users. + +#### Acceptance Criteria + +1. WHEN an administrator opens the edit dialog, THE Admin_System SHALL display the page title and content in a rich text editor +2. WHEN an administrator modifies the content and clicks save, THE Admin_System SHALL update the page content +3. IF the page ID is between 2 and 20, THEN THE Admin_System SHALL prevent editing the title field +4. THE Rich_Text_Editor SHALL support image upload and basic formatting + +### Requirement 3: 悬浮球列表 + +**User Story:** As an administrator, I want to view and manage float ball configurations, so that I can control the floating buttons on the homepage. + +#### Acceptance Criteria + +1. WHEN an administrator accesses the FloatBall list page, THE Admin_System SHALL display a list of all float ball configurations +2. THE Admin_System SHALL display title, image, background image, detail image, type, link URL, position (X/Y), size (width/height), detail image position and size, effect, status, and create time +3. THE Admin_System SHALL display images as thumbnails with preview capability +4. THE Admin_System SHALL support pagination for the float ball list + +### Requirement 4: 悬浮球新增 + +**User Story:** As an administrator, I want to create new float ball configurations, so that I can add floating buttons to the homepage. + +#### Acceptance Criteria + +1. WHEN an administrator clicks the add button, THE Admin_System SHALL open a form dialog for creating a new float ball +2. THE Admin_System SHALL require type, image, position X, position Y, width, height, and effect fields +3. THE Admin_System SHALL provide type selection (show image/jump to page) +4. THE Admin_System SHALL provide image upload for image, background image, and detail image +5. THE Admin_System SHALL provide link URL input when type is "jump to page" +6. THE Admin_System SHALL provide position and size inputs +7. THE Admin_System SHALL provide effect selection (none/scale animation) +8. THE Admin_System SHALL provide status toggle + +### Requirement 5: 悬浮球编辑 + +**User Story:** As an administrator, I want to edit existing float ball configurations, so that I can update floating button settings. + +#### Acceptance Criteria + +1. WHEN an administrator clicks the edit button, THE Admin_System SHALL open a form dialog with the configuration data pre-filled +2. THE Admin_System SHALL allow editing all fields +3. WHEN an administrator saves changes, THE Admin_System SHALL update the configuration + +### Requirement 6: 悬浮球状态管理 + +**User Story:** As an administrator, I want to manage float ball status, so that I can enable or disable floating buttons. + +#### Acceptance Criteria + +1. WHEN an administrator toggles the status switch, THE Admin_System SHALL update the float ball status immediately +2. WHEN an administrator clicks delete, THE Admin_System SHALL show a confirmation dialog and delete the configuration upon confirmation + +### Requirement 7: 福利屋入口列表 + +**User Story:** As an administrator, I want to view and manage welfare house entries, so that I can control the welfare house navigation. + +#### Acceptance Criteria + +1. WHEN an administrator accesses the WelfareHouse list page, THE Admin_System SHALL display a list of all welfare house entries +2. THE Admin_System SHALL display ID, name, image, URL, sort order, status, and create time +3. THE Admin_System SHALL display images as thumbnails with preview capability +4. THE Admin_System SHALL support pagination for the welfare house list + +### Requirement 8: 福利屋入口新增 + +**User Story:** As an administrator, I want to create new welfare house entries, so that I can add navigation items. + +#### Acceptance Criteria + +1. WHEN an administrator clicks the add button, THE Admin_System SHALL open a form dialog for creating a new entry +2. THE Admin_System SHALL require name, image, URL, and sort order fields +3. THE Admin_System SHALL provide image upload for the entry image +4. THE Admin_System SHALL provide status toggle + +### Requirement 9: 福利屋入口编辑 + +**User Story:** As an administrator, I want to edit existing welfare house entries, so that I can update navigation items. + +#### Acceptance Criteria + +1. WHEN an administrator clicks the edit button, THE Admin_System SHALL open a form dialog with the entry data pre-filled +2. THE Admin_System SHALL allow editing all fields +3. WHEN an administrator saves changes, THE Admin_System SHALL update the entry + +### Requirement 10: 福利屋入口状态管理 + +**User Story:** As an administrator, I want to manage welfare house entry status, so that I can enable or disable navigation items. + +#### Acceptance Criteria + +1. WHEN an administrator toggles the status switch, THE Admin_System SHALL update the entry status immediately +2. WHEN an administrator clicks delete, THE Admin_System SHALL show a confirmation dialog and delete the entry upon confirmation + +### Requirement 11: 后端API + +**User Story:** As a developer, I want backend APIs for all content and auxiliary modules, so that the frontend can interact with the data. + +#### Acceptance Criteria + +1. THE Admin_System SHALL provide Danye API endpoints: GET list, GET by ID, PUT update, PUT toggle image optimizer +2. THE Admin_System SHALL provide FloatBall API endpoints: GET list, GET by ID, POST create, PUT update, DELETE, PUT status +3. THE Admin_System SHALL provide WelfareHouse API endpoints: GET list, GET by ID, POST create, PUT update, DELETE, PUT status +4. THE Admin_System SHALL return consistent response format for all APIs +5. THE Admin_System SHALL validate required fields before saving + +### Requirement 12: 路由和菜单配置 + +**User Story:** As an administrator, I want to access content and auxiliary management pages from the menu, so that I can navigate to these features. + +#### Acceptance Criteria + +1. THE Admin_System SHALL add "内容管理" menu group with sub-menus for Danye, FloatBall, and WelfareHouse +2. THE Admin_System SHALL configure routes for all content and auxiliary pages +3. THE Admin_System SHALL configure appropriate permissions for each page diff --git a/.kiro/specs/content-auxiliary-frontend/tasks.md b/.kiro/specs/content-auxiliary-frontend/tasks.md new file mode 100644 index 00000000..4849a2af --- /dev/null +++ b/.kiro/specs/content-auxiliary-frontend/tasks.md @@ -0,0 +1,413 @@ +# Implementation Plan: 内容与辅助模块前端迁移 + +## Overview + +本实现计划将"内容与辅助"模块从老项目(PHP ThinkPHP + Layui)迁移到新项目(ASP.NET Core + Vue 3 + Element Plus)。该模块包含三个子模块:单页管理(Danye)、悬浮球配置(FloatBall)、福利屋入口(WelfareHouse)。 + +实现分为后端 API 开发和前端页面开发两个主要部分。 + +## 任务概览 + +| 阶段 | 任务数 | 预计工时 | +|------|--------|----------| +| 1. 后端单页管理API开发 | 4 | 2h | +| 2. 后端悬浮球API开发 | 4 | 2.5h | +| 3. 后端福利屋API开发 | 4 | 2h | +| 4. Checkpoint - 后端API验证 | 1 | 0.5h | +| 5. 前端API层开发 | 3 | 1.5h | +| 6. 前端单页管理页面开发 | 3 | 2.5h | +| 7. 前端悬浮球页面开发 | 4 | 3h | +| 8. 前端福利屋页面开发 | 4 | 2.5h | +| 9. Checkpoint - 前端页面验证 | 1 | 0.5h | +| 10. 路由和菜单配置 | 2 | 1h | +| 11. Checkpoint - 完整功能验证 | 1 | 0.5h | +| 12. 属性测试 | 4 | 1.5h | +| 13. Final Checkpoint | 1 | 0.5h | +| **总计** | **36** | **20.5h** | + +--- + +## Tasks + +- [x] 1. 后端单页管理API开发 + - [x] 1.1 创建单页管理Models + - 在 `HoneyBox.Admin.Business/Models/Danye/` 创建目录 + - 创建 `DanyeModels.cs` 定义请求和响应模型 + - 定义 `DanyeResponse` 列表响应模型(Id, Title, IsImageOptimizer, UpdateTime) + - 定义 `DanyeDetailResponse` 详情响应模型(Id, Title, Content, IsImageOptimizer) + - 定义 `DanyeUpdateRequest` 更新请求模型(Title?, Content) + - 定义 `ImageOptimizerRequest` 图片优化切换请求模型 + - _Requirements: 11.1_ + + - [x] 1.2 创建单页管理Service接口和实现 + - 在 `HoneyBox.Admin.Business/Services/Interfaces/` 创建 `IDanyeService.cs` + - 定义 `GetDanyeListAsync` 列表查询方法 + - 定义 `GetDanyeByIdAsync` 详情查询方法 + - 定义 `UpdateDanyeAsync` 更新方法 + - 定义 `ToggleImageOptimizerAsync` 切换图片优化方法 + - 在 `HoneyBox.Admin.Business/Services/` 创建 `DanyeService.cs` + - 实现所有方法,ID 2-20 的单页标题不可编辑 + - _Requirements: 1.1, 2.1, 2.2, 2.3, 11.1_ + + - [x] 1.3 创建单页管理Controller + - 在 `HoneyBox.Admin.Business/Controllers/` 创建 `DanyeController.cs` + - 实现 `GET /api/business/danye` 列表查询接口 + - 实现 `GET /api/business/danye/{id}` 详情查询接口 + - 实现 `PUT /api/business/danye/{id}` 更新接口 + - 实现 `PUT /api/business/danye/{id}/image-optimizer` 切换图片优化接口 + - _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 11.1_ + + - [x] 1.4 注册单页管理服务 + - 在 `ServiceCollectionExtensions.cs` 注册 `IDanyeService` 和 `DanyeService` + - _Requirements: 11.1_ + +- [x] 2. 后端悬浮球API开发 + - [x] 2.1 创建悬浮球Models + - 在 `HoneyBox.Admin.Business/Models/FloatBall/` 创建目录 + - 创建 `FloatBallModels.cs` 定义请求和响应模型 + - 定义 `FloatBallListRequest` 列表查询请求(Page, PageSize) + - 定义 `FloatBallResponse` 响应模型(所有字段) + - 定义 `FloatBallCreateRequest` 新增请求模型 + - 定义 `FloatBallUpdateRequest` 更新请求模型 + - 定义 `FloatBallStatusRequest` 状态切换请求模型 + - _Requirements: 11.2_ + + - [x] 2.2 创建悬浮球Service接口和实现 + - 在 `HoneyBox.Admin.Business/Services/Interfaces/` 创建 `IFloatBallService.cs` + - 定义 `GetFloatBallsAsync` 列表查询方法(分页) + - 定义 `GetFloatBallByIdAsync` 详情查询方法 + - 定义 `CreateFloatBallAsync` 新增方法 + - 定义 `UpdateFloatBallAsync` 更新方法 + - 定义 `DeleteFloatBallAsync` 删除方法 + - 定义 `UpdateStatusAsync` 状态切换方法 + - 在 `HoneyBox.Admin.Business/Services/` 创建 `FloatBallService.cs` + - 实现所有方法,验证必填字段 + - _Requirements: 3.1, 3.4, 4.1, 4.2, 5.1, 5.3, 6.1, 6.2, 11.2_ + + - [x] 2.3 创建悬浮球Controller + - 在 `HoneyBox.Admin.Business/Controllers/` 创建 `FloatBallController.cs` + - 实现 `GET /api/business/floatball` 列表查询接口(分页) + - 实现 `GET /api/business/floatball/{id}` 详情查询接口 + - 实现 `POST /api/business/floatball` 新增接口 + - 实现 `PUT /api/business/floatball/{id}` 更新接口 + - 实现 `DELETE /api/business/floatball/{id}` 删除接口 + - 实现 `PUT /api/business/floatball/{id}/status` 状态切换接口 + - _Requirements: 3.1, 3.2, 3.3, 3.4, 4.1, 4.2, 5.1, 5.3, 6.1, 6.2, 11.2_ + + - [x] 2.4 注册悬浮球服务 + - 在 `ServiceCollectionExtensions.cs` 注册 `IFloatBallService` 和 `FloatBallService` + - _Requirements: 11.2_ + +- [x] 3. 后端福利屋API开发 + - [x] 3.1 创建福利屋Models + - 在 `HoneyBox.Admin.Business/Models/WelfareHouse/` 创建目录 + - 创建 `WelfareHouseModels.cs` 定义请求和响应模型 + - 定义 `WelfareHouseListRequest` 列表查询请求(Page, PageSize) + - 定义 `WelfareHouseResponse` 响应模型(Id, Name, Image, Url, Sort, Status, CreateTime) + - 定义 `WelfareHouseCreateRequest` 新增请求模型 + - 定义 `WelfareHouseUpdateRequest` 更新请求模型 + - 定义 `WelfareHouseStatusRequest` 状态切换请求模型 + - _Requirements: 11.3_ + + - [x] 3.2 创建福利屋Service接口和实现 + - 在 `HoneyBox.Admin.Business/Services/Interfaces/` 创建 `IWelfareHouseService.cs` + - 定义 `GetWelfareHousesAsync` 列表查询方法(分页) + - 定义 `GetWelfareHouseByIdAsync` 详情查询方法 + - 定义 `CreateWelfareHouseAsync` 新增方法 + - 定义 `UpdateWelfareHouseAsync` 更新方法 + - 定义 `DeleteWelfareHouseAsync` 删除方法 + - 定义 `UpdateStatusAsync` 状态切换方法 + - 在 `HoneyBox.Admin.Business/Services/` 创建 `WelfareHouseService.cs` + - 实现所有方法,验证必填字段 + - _Requirements: 7.1, 7.4, 8.1, 8.2, 9.1, 9.3, 10.1, 10.2, 11.3_ + + - [x] 3.3 创建福利屋Controller + - 在 `HoneyBox.Admin.Business/Controllers/` 创建 `WelfareHouseController.cs` + - 实现 `GET /api/business/welfarehouse` 列表查询接口(分页) + - 实现 `GET /api/business/welfarehouse/{id}` 详情查询接口 + - 实现 `POST /api/business/welfarehouse` 新增接口 + - 实现 `PUT /api/business/welfarehouse/{id}` 更新接口 + - 实现 `DELETE /api/business/welfarehouse/{id}` 删除接口 + - 实现 `PUT /api/business/welfarehouse/{id}/status` 状态切换接口 + - _Requirements: 7.1, 7.2, 7.3, 7.4, 8.1, 8.2, 9.1, 9.3, 10.1, 10.2, 11.3_ + + - [x] 3.4 注册福利屋服务 + - 在 `ServiceCollectionExtensions.cs` 注册 `IWelfareHouseService` 和 `WelfareHouseService` + - _Requirements: 11.3_ + +- [x] 4. Checkpoint - 后端API验证 + - 确保所有API编译通过 + - 使用Swagger测试API基本功能 + - 确保权限验证正常工作 + +- [x] 5. 前端API层开发 + - [x] 5.1 创建单页管理API模块 + - 创建 `src/api/business/danye.ts` + - 定义 TypeScript 接口(DanyeResponse, DanyeDetailResponse, DanyeUpdateRequest) + - 实现 `getDanyeList` 列表查询API调用 + - 实现 `getDanyeById` 详情查询API调用 + - 实现 `updateDanye` 更新API调用 + - 实现 `toggleImageOptimizer` 切换图片优化API调用 + - _Requirements: 11.1_ + + - [x] 5.2 创建悬浮球API模块 + - 创建 `src/api/business/floatball.ts` + - 定义 TypeScript 接口(FloatBallListRequest, FloatBallResponse, FloatBallCreateRequest) + - 实现 `getFloatBalls` 列表查询API调用 + - 实现 `getFloatBallById` 详情查询API调用 + - 实现 `createFloatBall` 新增API调用 + - 实现 `updateFloatBall` 更新API调用 + - 实现 `deleteFloatBall` 删除API调用 + - 实现 `updateFloatBallStatus` 状态切换API调用 + - _Requirements: 11.2_ + + - [x] 5.3 创建福利屋API模块 + - 创建 `src/api/business/welfarehouse.ts` + - 定义 TypeScript 接口(WelfareHouseListRequest, WelfareHouseResponse, WelfareHouseCreateRequest) + - 实现 `getWelfareHouses` 列表查询API调用 + - 实现 `getWelfareHouseById` 详情查询API调用 + - 实现 `createWelfareHouse` 新增API调用 + - 实现 `updateWelfareHouse` 更新API调用 + - 实现 `deleteWelfareHouse` 删除API调用 + - 实现 `updateWelfareHouseStatus` 状态切换API调用 + - _Requirements: 11.3_ + +- [x] 6. 前端单页管理页面开发 + - [x] 6.1 创建单页列表主页面 + - 创建 `src/views/business/danye/list.vue` + - 实现页面布局(表格区) + - 集成表格组件 + - 实现编辑操作 + - 实现图片优化开关切换 + - _Requirements: 1.1, 1.2, 1.3, 1.4_ + + - [x] 6.2 创建单页表格组件 + - 创建 `src/views/business/danye/components/DanyeTable.vue` + - 实现表格列配置(ID、标题、图片优化开关、更新时间、操作) + - 实现图片优化开关组件 + - 实现编辑按钮 + - _Requirements: 1.1, 1.4_ + + - [x] 6.3 创建单页编辑弹窗 + - 创建 `src/views/business/danye/components/DanyeFormDialog.vue` + - 实现标题输入框(ID 2-20 禁用) + - 集成富文本编辑器 + - 实现表单验证 + - 实现提交逻辑 + - _Requirements: 2.1, 2.2, 2.3, 2.4_ + +- [x] 7. 前端悬浮球页面开发 + - [x] 7.1 创建悬浮球列表主页面 + - 创建 `src/views/business/floatball/list.vue` + - 实现页面布局(新增按钮 + 表格区) + - 集成表格组件 + - 实现分页逻辑 + - 实现新增、编辑、删除操作 + - _Requirements: 3.1, 3.4_ + + - [x] 7.2 创建悬浮球表格组件 + - 创建 `src/views/business/floatball/components/FloatBallTable.vue` + - 实现表格列配置(ID、标题、图片、背景图、详情图、类型、链接、位置、尺寸、特效、状态、创建时间、操作) + - 实现图片预览功能 + - 实现状态开关组件 + - 实现操作按钮(编辑、删除) + - 实现删除确认弹窗 + - _Requirements: 3.1, 3.2, 3.3, 6.1, 6.2_ + + - [x] 7.3 创建悬浮球表单弹窗 + - 创建 `src/views/business/floatball/components/FloatBallFormDialog.vue` + - 实现标题输入 + - 实现类型选择(展示图片/跳转页面) + - 实现图片上传(悬浮球图片、背景图、详情图) + - 实现跳转链接输入(类型为跳转时显示) + - 实现位置输入(X、Y) + - 实现尺寸输入(宽、高) + - 实现详情图位置和尺寸输入 + - 实现特效选择(无/缩放动画) + - 实现状态开关 + - 实现表单验证 + - 实现提交逻辑 + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 5.1, 5.2, 5.3_ + + - [x] 7.4 实现悬浮球类型条件显示 + - 当类型为"展示图片"时,隐藏跳转链接输入 + - 当类型为"跳转页面"时,显示跳转链接输入 + - _Requirements: 4.5_ + +- [x] 8. 前端福利屋页面开发 + - [x] 8.1 创建福利屋列表主页面 + - 创建 `src/views/business/welfarehouse/list.vue` + - 实现页面布局(新增按钮 + 表格区) + - 集成表格组件 + - 实现分页逻辑 + - 实现新增、编辑、删除操作 + - _Requirements: 7.1, 7.4_ + + - [x] 8.2 创建福利屋表格组件 + - 创建 `src/views/business/welfarehouse/components/WelfareHouseTable.vue` + - 实现表格列配置(ID、名称、图片、链接、排序、状态、创建时间、操作) + - 实现图片预览功能 + - 实现状态开关组件 + - 实现操作按钮(编辑、删除) + - 实现删除确认弹窗 + - _Requirements: 7.1, 7.2, 7.3, 10.1, 10.2_ + + - [x] 8.3 创建福利屋表单弹窗 + - 创建 `src/views/business/welfarehouse/components/WelfareHouseFormDialog.vue` + - 实现名称输入 + - 实现图片上传 + - 实现跳转链接输入 + - 实现排序输入 + - 实现状态开关 + - 实现表单验证 + - 实现提交逻辑 + - _Requirements: 8.1, 8.2, 8.3, 8.4, 9.1, 9.2, 9.3_ + + - [x] 8.4 实现福利屋排序功能 + - 列表按排序值升序显示 + - _Requirements: 7.2_ + +- [x] 9. Checkpoint - 前端页面验证 + - 确保单页管理列表页面正常显示 + - 确保单页编辑功能正常(富文本编辑器) + - 确保图片优化开关功能正常 + - 确保悬浮球列表页面正常显示 + - 确保悬浮球新增、编辑、删除功能正常 + - 确保悬浮球类型条件显示正常 + - 确保福利屋列表页面正常显示 + - 确保福利屋新增、编辑、删除功能正常 + - 测试分页功能 + +- [x] 10. 路由和菜单配置 + - [x] 10.1 配置路由 + - 在 `src/router/modules/business.ts` 添加内容管理路由配置 + - 配置单页管理路由 `/business/danye/list` + - 配置悬浮球配置路由 `/business/floatball/list` + - 配置福利屋入口路由 `/business/welfarehouse/list` + - 配置权限标识 + - _Requirements: 12.1, 12.2, 12.3_ + + - [x] 10.2 创建菜单SQL脚本并执行 + - 在 `HoneyBox/scripts/` 创建 `seed_content_auxiliary_menus.sql` + - 添加"内容管理"菜单组 + - 添加单页管理菜单项 + - 添加悬浮球配置菜单项 + - 添加福利屋入口菜单项 + - 添加相关权限配置 + - 执行SQL脚本 + - _Requirements: 12.1, 12.2, 12.3_ + +- [x] 11. Checkpoint - 完整功能验证 + - 确保所有页面正常访问 + - 测试完整的单页管理流程 + - 测试完整的悬浮球配置流程 + - 测试完整的福利屋入口流程 + - 验证权限控制正常工作 + - 验证菜单显示正常 + +- [x] 12. 属性测试 + - [x] 12.1 编写分页参数传递属性测试 + - **Property 1: 分页参数正确传递** + - 验证悬浮球和福利屋列表分页参数正确传递 + - **Validates: Requirements 3.4, 7.4** + + - [x] 12.2 编写表单必填字段验证属性测试 + - **Property 2: 表单必填字段验证** + - 验证悬浮球表单必填字段(类型、图片、位置、尺寸、特效) + - 验证福利屋表单必填字段(名称、图片、链接、排序) + - **Validates: Requirements 4.2, 8.2** + + - [x] 12.3 编写条件显示字段属性测试 + - **Property 3: 条件显示字段正确切换** + - 验证悬浮球类型切换时跳转链接字段的显示/隐藏 + - **Validates: Requirements 4.5** + + - [x] 12.4 编写API响应格式一致性属性测试 + - **Property 4: API响应格式一致性** + - 验证所有API返回统一的响应格式 + - **Validates: Requirements 11.4, 11.5** + +- [x] 13. Final Checkpoint - 最终验证 + - 确保所有测试通过 + - 确保所有功能正常工作 + - 如有问题,询问用户 + +--- + +## 验收检查清单 + +### 功能验收 +- [ ] 单页管理列表正常显示 +- [ ] 单页编辑功能正常(富文本编辑器) +- [ ] 单页标题编辑限制正常(ID 2-20 不可编辑) +- [ ] 图片优化开关功能正常 +- [ ] 悬浮球列表正常显示,支持分页 +- [ ] 悬浮球新增功能正常 +- [ ] 悬浮球编辑功能正常 +- [ ] 悬浮球删除功能正常(有二次确认) +- [ ] 悬浮球状态切换功能正常 +- [ ] 悬浮球类型条件显示正常 +- [ ] 福利屋列表正常显示,支持分页 +- [ ] 福利屋新增功能正常 +- [ ] 福利屋编辑功能正常 +- [ ] 福利屋删除功能正常(有二次确认) +- [ ] 福利屋状态切换功能正常 +- [ ] 福利屋排序功能正常 + +### 非功能验收 +- [ ] 分页加载性能正常 +- [ ] 图片预览功能正常 +- [ ] 图片懒加载生效 +- [ ] 表单验证实时反馈 +- [ ] 操作提示正确显示 +- [ ] 危险操作有二次确认 +- [x] 属性测试全部通过 + +## Notes + +- 每个Checkpoint确保增量验证 +- 后端API需要先完成才能进行前端开发 +- 属性测试验证通用正确性属性 +- 所有任务均为必需任务,确保全面测试覆盖 +- 单页管理不需要新增/删除功能,只需要编辑 +- 悬浮球和福利屋需要完整的CRUD功能 +- 富文本编辑器需要确认项目中使用的编辑器类型 +- 数据库实体已存在,无需创建迁移 + +## 文件路径参考 + +### 后端文件 +- `server/HoneyBox/src/HoneyBox.Admin.Business/Models/Danye/DanyeModels.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Models/FloatBall/FloatBallModels.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Models/WelfareHouse/WelfareHouseModels.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IDanyeService.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IFloatBallService.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IWelfareHouseService.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/DanyeService.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/FloatBallService.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/WelfareHouseService.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/DanyeController.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/FloatBallController.cs` +- `server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/WelfareHouseController.cs` + +### 前端文件 +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/danye.ts` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/floatball.ts` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/welfarehouse.ts` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/list.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeTable.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeFormDialog.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/list.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallTable.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallFormDialog.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/list.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseTable.vue` +- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseFormDialog.vue` + +### 参考文件 +- PHP 单页管理: `server/php/app/admin/controller/Danye.php` +- PHP 悬浮球: `server/php/app/admin/controller/FloatBall.php` +- PHP 福利屋: `server/php/app/admin/controller/WelfareHouse.php` +- 实体 Danye: `server/HoneyBox/src/HoneyBox.Model/Entities/Danye.cs` +- 实体 FloatBallConfig: `server/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs` +- 实体 WelfareHouse: `server/HoneyBox/src/HoneyBox.Model/Entities/WelfareHouse.cs` diff --git a/server/HoneyBox/scripts/seed_content_auxiliary_menus.sql b/server/HoneyBox/scripts/seed_content_auxiliary_menus.sql new file mode 100644 index 00000000..1fdd4c0b --- /dev/null +++ b/server/HoneyBox/scripts/seed_content_auxiliary_menus.sql @@ -0,0 +1,203 @@ +-- ============================================= +-- 内容与辅助模块菜单初始化脚本 +-- 用于在后台管理系统中添加内容管理相关菜单 +-- 包含:单页管理、悬浮球配置、福利屋入口 +-- +-- 注意:此脚本需要在 Admin 数据库中执行 +-- 表名使用小写(menus, roles, permissions, role_menus, role_permissions) +-- ============================================= + +-- 注意:执行此脚本前请确保: +-- 1. 已存在超级管理员角色 (Code = 'super_admin') +-- 2. 数据库中已有基础菜单数据 + +-- 声明变量 +DECLARE @ContentMenuId BIGINT; +DECLARE @SuperAdminRoleId BIGINT; + +-- 获取超级管理员角色ID +SELECT @SuperAdminRoleId = Id FROM roles WHERE Code = 'super_admin'; + +-- ============================================= +-- 1. 创建内容管理目录(顶级菜单) +-- ============================================= +IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/content') +BEGIN + INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt) + VALUES (0, N'内容管理', '/business/content', 'Layout', 'Document', 1, NULL, 70, 1, 0, 0, GETDATE()); + + SET @ContentMenuId = SCOPE_IDENTITY(); + PRINT N'创建内容管理目录,ID: ' + CAST(@ContentMenuId AS VARCHAR); +END +ELSE +BEGIN + SELECT @ContentMenuId = Id FROM menus WHERE Path = '/business/content'; + UPDATE menus SET ParentId = 0, Component = 'Layout' WHERE Id = @ContentMenuId AND ParentId <> 0; + PRINT N'内容管理目录已存在,ID: ' + CAST(@ContentMenuId AS VARCHAR); +END + +-- ============================================= +-- 2. 创建单页管理子菜单 +-- ============================================= +IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/danye/list') +BEGIN + INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt) + VALUES (@ContentMenuId, N'单页管理', '/business/danye/list', 'business/danye/list', 'Notebook', 2, 'danye:list', 1, 1, 0, 1, GETDATE()); + PRINT N'创建单页管理菜单'; +END + +-- ============================================= +-- 3. 创建悬浮球配置子菜单 +-- ============================================= +IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/floatball/list') +BEGIN + INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt) + VALUES (@ContentMenuId, N'悬浮球配置', '/business/floatball/list', 'business/floatball/list', 'Pointer', 2, 'floatball:list', 2, 1, 0, 1, GETDATE()); + PRINT N'创建悬浮球配置菜单'; +END + +-- ============================================= +-- 4. 创建福利屋入口子菜单 +-- ============================================= +IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/welfarehouse/list') +BEGIN + INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt) + VALUES (@ContentMenuId, N'福利屋入口', '/business/welfarehouse/list', 'business/welfarehouse/list', 'Present', 2, 'welfarehouse:list', 3, 1, 0, 1, GETDATE()); + PRINT N'创建福利屋入口菜单'; +END + +-- ============================================= +-- 5. 添加单页管理相关权限 +-- ============================================= +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'danye:list') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'单页列表', 'danye:list', N'内容管理', GETDATE()); +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'danye:edit') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'编辑单页', 'danye:edit', N'内容管理', GETDATE()); + +-- ============================================= +-- 6. 添加悬浮球配置相关权限 +-- ============================================= +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:list') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'悬浮球列表', 'floatball:list', N'内容管理', GETDATE()); +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:add') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'新增悬浮球', 'floatball:add', N'内容管理', GETDATE()); +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:edit') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'编辑悬浮球', 'floatball:edit', N'内容管理', GETDATE()); +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:delete') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'删除悬浮球', 'floatball:delete', N'内容管理', GETDATE()); + +-- ============================================= +-- 7. 添加福利屋入口相关权限 +-- ============================================= +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:list') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'福利屋列表', 'welfarehouse:list', N'内容管理', GETDATE()); +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:add') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'新增福利屋入口', 'welfarehouse:add', N'内容管理', GETDATE()); +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:edit') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'编辑福利屋入口', 'welfarehouse:edit', N'内容管理', GETDATE()); +IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:delete') + INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'删除福利屋入口', 'welfarehouse:delete', N'内容管理', GETDATE()); + +-- ============================================= +-- 8. 为超级管理员角色分配新菜单 +-- ============================================= +IF @SuperAdminRoleId IS NOT NULL +BEGIN + -- 分配内容管理目录 + IF NOT EXISTS (SELECT 1 FROM role_menus WHERE RoleId = @SuperAdminRoleId AND MenuId = @ContentMenuId) + BEGIN + INSERT INTO role_menus (RoleId, MenuId) VALUES (@SuperAdminRoleId, @ContentMenuId); + PRINT N'为超级管理员分配内容管理目录'; + END + + -- 分配所有新创建的子菜单 + INSERT INTO role_menus (RoleId, MenuId) + SELECT @SuperAdminRoleId, m.Id + FROM menus m + WHERE m.ParentId = @ContentMenuId + AND NOT EXISTS (SELECT 1 FROM role_menus rm WHERE rm.RoleId = @SuperAdminRoleId AND rm.MenuId = m.Id); + + PRINT N'为超级管理员分配内容管理子菜单'; + + -- 分配新增的权限 + INSERT INTO role_permissions (RoleId, PermissionId) + SELECT @SuperAdminRoleId, p.Id + FROM permissions p + WHERE p.Code IN ( + 'danye:list', 'danye:edit', + 'floatball:list', 'floatball:add', 'floatball:edit', 'floatball:delete', + 'welfarehouse:list', 'welfarehouse:add', 'welfarehouse:edit', 'welfarehouse:delete' + ) + AND NOT EXISTS ( + SELECT 1 FROM role_permissions rp + WHERE rp.RoleId = @SuperAdminRoleId AND rp.PermissionId = p.Id + ); + + PRINT N'为超级管理员分配内容管理权限'; +END + +-- ============================================= +-- 9. 验证结果 +-- ============================================= +PRINT N''; +PRINT N'========== 菜单创建结果 =========='; +SELECT + m.Id, + m.ParentId, + m.Name, + m.Path, + m.Component, + m.MenuType, + m.Permission, + m.SortOrder +FROM menus m +WHERE m.Path LIKE '/business/content%' + OR m.Path LIKE '/business/danye%' + OR m.Path LIKE '/business/floatball%' + OR m.Path LIKE '/business/welfarehouse%' +ORDER BY m.ParentId, m.SortOrder; + +PRINT N''; +PRINT N'========== 权限创建结果 =========='; +SELECT + p.Id, + p.Name, + p.Code, + p.Module +FROM permissions p +WHERE p.Code LIKE 'danye:%' + OR p.Code LIKE 'floatball:%' + OR p.Code LIKE 'welfarehouse:%' +ORDER BY p.Code; + +PRINT N''; +PRINT N'========== 角色菜单分配结果 =========='; +SELECT + r.Name AS RoleName, + m.Name AS MenuName, + m.Path +FROM role_menus rm +INNER JOIN roles r ON rm.RoleId = r.Id +INNER JOIN menus m ON rm.MenuId = m.Id +WHERE m.Path LIKE '/business/content%' + OR m.Path LIKE '/business/danye%' + OR m.Path LIKE '/business/floatball%' + OR m.Path LIKE '/business/welfarehouse%' +ORDER BY r.Name, m.Path; + +PRINT N''; +PRINT N'========== 角色权限分配结果 =========='; +SELECT + r.Name AS RoleName, + p.Name AS PermissionName, + p.Code +FROM role_permissions rp +INNER JOIN roles r ON rp.RoleId = r.Id +INNER JOIN permissions p ON rp.PermissionId = p.Id +WHERE p.Code LIKE 'danye:%' + OR p.Code LIKE 'floatball:%' + OR p.Code LIKE 'welfarehouse:%' +ORDER BY r.Name, p.Code; + +PRINT N''; +PRINT N'内容与辅助模块菜单初始化完成'; diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/DanyeController.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/DanyeController.cs new file mode 100644 index 00000000..63ab823d --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/DanyeController.cs @@ -0,0 +1,106 @@ +using HoneyBox.Admin.Business.Attributes; +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.Danye; +using HoneyBox.Admin.Business.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace HoneyBox.Admin.Business.Controllers; + +/// +/// 单页管理控制器 +/// +[Route("api/admin/business/[controller]")] +public class DanyeController : BusinessControllerBase +{ + private readonly IDanyeService _danyeService; + + public DanyeController(IDanyeService danyeService) + { + _danyeService = danyeService; + } + + /// + /// 获取单页列表 + /// + /// 单页列表 + [HttpGet] + [BusinessPermission("danye:list")] + public async Task GetDanyeList() + { + try + { + var result = await _danyeService.GetDanyeListAsync(); + return Ok(result); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 获取单页详情 + /// + /// 单页ID + /// 单页详情 + [HttpGet("{id}")] + [BusinessPermission("danye:list")] + public async Task GetDanyeById(int id) + { + try + { + var result = await _danyeService.GetDanyeByIdAsync(id); + if (result == null) + { + return NotFoundError("单页不存在"); + } + return Ok(result); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 更新单页内容 + /// + /// 单页ID + /// 更新请求 + /// 操作结果 + [HttpPut("{id}")] + [BusinessPermission("danye:edit")] + public async Task UpdateDanye(int id, [FromBody] DanyeUpdateRequest request) + { + try + { + var result = await _danyeService.UpdateDanyeAsync(id, request); + return result ? Ok("更新成功") : Error(BusinessErrorCodes.InternalError, "更新失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 切换图片优化状态 + /// + /// 单页ID + /// 图片优化请求 + /// 操作结果 + [HttpPut("{id}/image-optimizer")] + [BusinessPermission("danye:edit")] + public async Task ToggleImageOptimizer(int id, [FromBody] ImageOptimizerRequest request) + { + try + { + var result = await _danyeService.ToggleImageOptimizerAsync(id, request); + return result ? Ok("切换成功") : Error(BusinessErrorCodes.InternalError, "切换失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/FloatBallController.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/FloatBallController.cs new file mode 100644 index 00000000..aa910c76 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/FloatBallController.cs @@ -0,0 +1,147 @@ +using HoneyBox.Admin.Business.Attributes; +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.FloatBall; +using HoneyBox.Admin.Business.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace HoneyBox.Admin.Business.Controllers; + +/// +/// 悬浮球配置控制器 +/// +[Route("api/admin/business/[controller]")] +public class FloatBallController : BusinessControllerBase +{ + private readonly IFloatBallService _floatBallService; + + public FloatBallController(IFloatBallService floatBallService) + { + _floatBallService = floatBallService; + } + + /// + /// 获取悬浮球列表(分页) + /// + /// 分页请求 + /// 悬浮球列表 + [HttpGet] + [BusinessPermission("floatball:list")] + public async Task GetFloatBalls([FromQuery] FloatBallListRequest request) + { + try + { + var result = await _floatBallService.GetFloatBallsAsync(request); + return Ok(result); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 获取悬浮球详情 + /// + /// 悬浮球ID + /// 悬浮球详情 + [HttpGet("{id}")] + [BusinessPermission("floatball:list")] + public async Task GetFloatBallById(int id) + { + try + { + var result = await _floatBallService.GetFloatBallByIdAsync(id); + if (result == null) + { + return NotFoundError("悬浮球配置不存在"); + } + return Ok(result); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 新增悬浮球 + /// + /// 创建请求 + /// 新创建的悬浮球ID + [HttpPost] + [BusinessPermission("floatball:add")] + public async Task CreateFloatBall([FromBody] FloatBallCreateRequest request) + { + try + { + var id = await _floatBallService.CreateFloatBallAsync(request); + return Ok(new { id }, "创建成功"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 更新悬浮球 + /// + /// 悬浮球ID + /// 更新请求 + /// 操作结果 + [HttpPut("{id}")] + [BusinessPermission("floatball:edit")] + public async Task UpdateFloatBall(int id, [FromBody] FloatBallUpdateRequest request) + { + try + { + var result = await _floatBallService.UpdateFloatBallAsync(id, request); + return result ? Ok("更新成功") : Error(BusinessErrorCodes.InternalError, "更新失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 删除悬浮球 + /// + /// 悬浮球ID + /// 操作结果 + [HttpDelete("{id}")] + [BusinessPermission("floatball:delete")] + public async Task DeleteFloatBall(int id) + { + try + { + var result = await _floatBallService.DeleteFloatBallAsync(id); + return result ? Ok("删除成功") : Error(BusinessErrorCodes.InternalError, "删除失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 切换悬浮球状态 + /// + /// 悬浮球ID + /// 状态请求 + /// 操作结果 + [HttpPut("{id}/status")] + [BusinessPermission("floatball:edit")] + public async Task UpdateStatus(int id, [FromBody] FloatBallStatusRequest request) + { + try + { + var result = await _floatBallService.UpdateStatusAsync(id, request); + return result ? Ok("状态更新成功") : Error(BusinessErrorCodes.InternalError, "状态更新失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/WelfareHouseController.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/WelfareHouseController.cs new file mode 100644 index 00000000..25ec82b0 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/WelfareHouseController.cs @@ -0,0 +1,148 @@ +using HoneyBox.Admin.Business.Attributes; +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.WelfareHouse; +using HoneyBox.Admin.Business.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace HoneyBox.Admin.Business.Controllers; + +/// +/// 福利屋入口控制器 +/// +[Route("api/admin/business/[controller]")] +public class WelfareHouseController : BusinessControllerBase +{ + private readonly IWelfareHouseService _welfareHouseService; + + public WelfareHouseController(IWelfareHouseService welfareHouseService) + { + _welfareHouseService = welfareHouseService; + } + + /// + /// 获取福利屋入口列表(分页) + /// + /// 分页请求 + /// 福利屋入口列表 + [HttpGet] + [BusinessPermission("welfarehouse:list")] + public async Task GetWelfareHouses([FromQuery] WelfareHouseListRequest request) + { + try + { + var result = await _welfareHouseService.GetWelfareHousesAsync(request); + return Ok(result); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 获取福利屋入口详情 + /// + /// 福利屋入口ID + /// 福利屋入口详情 + [HttpGet("{id}")] + [BusinessPermission("welfarehouse:list")] + public async Task GetWelfareHouseById(int id) + { + try + { + var result = await _welfareHouseService.GetWelfareHouseByIdAsync(id); + if (result == null) + { + return NotFoundError("福利屋入口不存在"); + } + return Ok(result); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + + /// + /// 新增福利屋入口 + /// + /// 创建请求 + /// 新创建的福利屋入口ID + [HttpPost] + [BusinessPermission("welfarehouse:add")] + public async Task CreateWelfareHouse([FromBody] WelfareHouseCreateRequest request) + { + try + { + var id = await _welfareHouseService.CreateWelfareHouseAsync(request); + return Ok(new { id }, "创建成功"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 更新福利屋入口 + /// + /// 福利屋入口ID + /// 更新请求 + /// 操作结果 + [HttpPut("{id}")] + [BusinessPermission("welfarehouse:edit")] + public async Task UpdateWelfareHouse(int id, [FromBody] WelfareHouseUpdateRequest request) + { + try + { + var result = await _welfareHouseService.UpdateWelfareHouseAsync(id, request); + return result ? Ok("更新成功") : Error(BusinessErrorCodes.InternalError, "更新失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 删除福利屋入口 + /// + /// 福利屋入口ID + /// 操作结果 + [HttpDelete("{id}")] + [BusinessPermission("welfarehouse:delete")] + public async Task DeleteWelfareHouse(int id) + { + try + { + var result = await _welfareHouseService.DeleteWelfareHouseAsync(id); + return result ? Ok("删除成功") : Error(BusinessErrorCodes.InternalError, "删除失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } + + /// + /// 切换福利屋入口状态 + /// + /// 福利屋入口ID + /// 状态请求 + /// 操作结果 + [HttpPut("{id}/status")] + [BusinessPermission("welfarehouse:edit")] + public async Task UpdateStatus(int id, [FromBody] WelfareHouseStatusRequest request) + { + try + { + var result = await _welfareHouseService.UpdateStatusAsync(id, request); + return result ? Ok("状态更新成功") : Error(BusinessErrorCodes.InternalError, "状态更新失败"); + } + catch (BusinessException ex) + { + return Error(ex.Code, ex.Message); + } + } +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Danye/DanyeModels.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Danye/DanyeModels.cs new file mode 100644 index 00000000..b664d581 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Danye/DanyeModels.cs @@ -0,0 +1,93 @@ +namespace HoneyBox.Admin.Business.Models.Danye; + +#region Response Models + +/// +/// 单页列表响应模型 +/// +public class DanyeResponse +{ + /// + /// 单页ID + /// + public int Id { get; set; } + + /// + /// 标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 是否启用图片优化 + /// + public bool IsImageOptimizer { get; set; } + + /// + /// 更新时间 + /// + public DateTime UpdateTime { get; set; } +} + +/// +/// 单页详情响应模型 +/// +public class DanyeDetailResponse +{ + /// + /// 单页ID + /// + public int Id { get; set; } + + /// + /// 标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 内容(富文本HTML) + /// + public string Content { get; set; } = string.Empty; + + /// + /// 是否启用图片优化 + /// + public bool IsImageOptimizer { get; set; } + + /// + /// 标题是否可编辑(ID 2-20 不可编辑) + /// + public bool IsTitleEditable { get; set; } +} + +#endregion + +#region Request Models + +/// +/// 单页更新请求模型 +/// +public class DanyeUpdateRequest +{ + /// + /// 标题(可选,ID 2-20 的单页标题不可编辑) + /// + public string? Title { get; set; } + + /// + /// 内容(富文本HTML) + /// + public string Content { get; set; } = string.Empty; +} + +/// +/// 图片优化切换请求模型 +/// +public class ImageOptimizerRequest +{ + /// + /// 是否启用图片优化 + /// + public bool IsImageOptimizer { get; set; } +} + +#endregion diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Models/FloatBall/FloatBallModels.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/FloatBall/FloatBallModels.cs new file mode 100644 index 00000000..2c70edab --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/FloatBall/FloatBallModels.cs @@ -0,0 +1,295 @@ +namespace HoneyBox.Admin.Business.Models.FloatBall; + +#region Response Models + +/// +/// 悬浮球列表响应模型 +/// +public class FloatBallResponse +{ + /// + /// 主键ID + /// + public int Id { get; set; } + + /// + /// 标题 + /// + public string? Title { get; set; } + + /// + /// 类型: 1展示图片 2跳转页面 + /// + public int Type { get; set; } + + /// + /// 悬浮球图片URL + /// + public string Image { get; set; } = string.Empty; + + /// + /// 背景图片URL + /// + public string? ImageBj { get; set; } + + /// + /// 详情图片URL + /// + public string? ImageDetails { get; set; } + + /// + /// 跳转链接 + /// + public string LinkUrl { get; set; } = string.Empty; + + /// + /// X轴位置 + /// + public string PositionX { get; set; } = string.Empty; + + /// + /// Y轴位置 + /// + public string PositionY { get; set; } = string.Empty; + + /// + /// 宽度 + /// + public string Width { get; set; } = string.Empty; + + /// + /// 高度 + /// + public string Height { get; set; } = string.Empty; + + /// + /// 详情图片X偏移 + /// + public string? ImageDetailsX { get; set; } + + /// + /// 详情图片Y偏移 + /// + public string? ImageDetailsY { get; set; } + + /// + /// 详情图片宽度 + /// + public string? ImageDetailsW { get; set; } + + /// + /// 详情图片高度 + /// + public string? ImageDetailsH { get; set; } + + /// + /// 特效: 0无 1缩放动画 + /// + public int Effect { get; set; } + + /// + /// 状态: 0关闭 1开启 + /// + public int Status { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } +} + +#endregion + +#region Request Models + +/// +/// 悬浮球列表查询请求 +/// +public class FloatBallListRequest : PagedRequest +{ +} + +/// +/// 悬浮球创建请求模型 +/// +public class FloatBallCreateRequest +{ + /// + /// 标题 + /// + public string? Title { get; set; } + + /// + /// 类型: 1展示图片 2跳转页面 + /// + public int Type { get; set; } + + /// + /// 悬浮球图片URL(必填) + /// + public string Image { get; set; } = string.Empty; + + /// + /// 背景图片URL + /// + public string? ImageBj { get; set; } + + /// + /// 详情图片URL + /// + public string? ImageDetails { get; set; } + + /// + /// 跳转链接(类型为跳转页面时使用) + /// + public string? LinkUrl { get; set; } + + /// + /// X轴位置(必填) + /// + public string PositionX { get; set; } = string.Empty; + + /// + /// Y轴位置(必填) + /// + public string PositionY { get; set; } = string.Empty; + + /// + /// 宽度(必填) + /// + public string Width { get; set; } = string.Empty; + + /// + /// 高度(必填) + /// + public string Height { get; set; } = string.Empty; + + /// + /// 详情图片X偏移 + /// + public string? ImageDetailsX { get; set; } + + /// + /// 详情图片Y偏移 + /// + public string? ImageDetailsY { get; set; } + + /// + /// 详情图片宽度 + /// + public string? ImageDetailsW { get; set; } + + /// + /// 详情图片高度 + /// + public string? ImageDetailsH { get; set; } + + /// + /// 特效: 0无 1缩放动画(必填) + /// + public int Effect { get; set; } + + /// + /// 状态: 0关闭 1开启,默认开启 + /// + public int Status { get; set; } = 1; +} + +/// +/// 悬浮球更新请求模型 +/// +public class FloatBallUpdateRequest +{ + /// + /// 标题 + /// + public string? Title { get; set; } + + /// + /// 类型: 1展示图片 2跳转页面 + /// + public int Type { get; set; } + + /// + /// 悬浮球图片URL(必填) + /// + public string Image { get; set; } = string.Empty; + + /// + /// 背景图片URL + /// + public string? ImageBj { get; set; } + + /// + /// 详情图片URL + /// + public string? ImageDetails { get; set; } + + /// + /// 跳转链接(类型为跳转页面时使用) + /// + public string? LinkUrl { get; set; } + + /// + /// X轴位置(必填) + /// + public string PositionX { get; set; } = string.Empty; + + /// + /// Y轴位置(必填) + /// + public string PositionY { get; set; } = string.Empty; + + /// + /// 宽度(必填) + /// + public string Width { get; set; } = string.Empty; + + /// + /// 高度(必填) + /// + public string Height { get; set; } = string.Empty; + + /// + /// 详情图片X偏移 + /// + public string? ImageDetailsX { get; set; } + + /// + /// 详情图片Y偏移 + /// + public string? ImageDetailsY { get; set; } + + /// + /// 详情图片宽度 + /// + public string? ImageDetailsW { get; set; } + + /// + /// 详情图片高度 + /// + public string? ImageDetailsH { get; set; } + + /// + /// 特效: 0无 1缩放动画(必填) + /// + public int Effect { get; set; } + + /// + /// 状态: 0关闭 1开启 + /// + public int Status { get; set; } +} + +/// +/// 悬浮球状态切换请求模型 +/// +public class FloatBallStatusRequest +{ + /// + /// 状态: 0关闭 1开启 + /// + public int Status { get; set; } +} + +#endregion diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Models/WelfareHouse/WelfareHouseModels.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/WelfareHouse/WelfareHouseModels.cs new file mode 100644 index 00000000..db3f1bfb --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/WelfareHouse/WelfareHouseModels.cs @@ -0,0 +1,130 @@ +namespace HoneyBox.Admin.Business.Models.WelfareHouse; + +#region Response Models + +/// +/// 福利屋入口列表响应模型 +/// +public class WelfareHouseResponse +{ + /// + /// 主键ID + /// + public int Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 图片URL + /// + public string Image { get; set; } = string.Empty; + + /// + /// 跳转链接 + /// + public string Url { get; set; } = string.Empty; + + /// + /// 排序值 + /// + public int Sort { get; set; } + + /// + /// 状态: 0禁用 1启用 + /// + public int Status { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } +} + +#endregion + +#region Request Models + +/// +/// 福利屋入口列表查询请求 +/// +public class WelfareHouseListRequest : PagedRequest +{ +} + +/// +/// 福利屋入口创建请求模型 +/// +public class WelfareHouseCreateRequest +{ + /// + /// 名称(必填) + /// + public string Name { get; set; } = string.Empty; + + /// + /// 图片URL(必填) + /// + public string Image { get; set; } = string.Empty; + + /// + /// 跳转链接(必填) + /// + public string Url { get; set; } = string.Empty; + + /// + /// 排序值(必填) + /// + public int Sort { get; set; } + + /// + /// 状态: 0禁用 1启用,默认启用 + /// + public int Status { get; set; } = 1; +} + +/// +/// 福利屋入口更新请求模型 +/// +public class WelfareHouseUpdateRequest +{ + /// + /// 名称(必填) + /// + public string Name { get; set; } = string.Empty; + + /// + /// 图片URL(必填) + /// + public string Image { get; set; } = string.Empty; + + /// + /// 跳转链接(必填) + /// + public string Url { get; set; } = string.Empty; + + /// + /// 排序值(必填) + /// + public int Sort { get; set; } + + /// + /// 状态: 0禁用 1启用 + /// + public int Status { get; set; } +} + +/// +/// 福利屋入口状态切换请求模型 +/// +public class WelfareHouseStatusRequest +{ + /// + /// 状态: 0禁用 1启用 + /// + public int Status { get; set; } +} + +#endregion diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Services/DanyeService.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/DanyeService.cs new file mode 100644 index 00000000..149e7d4f --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/DanyeService.cs @@ -0,0 +1,157 @@ +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.Danye; +using HoneyBox.Admin.Business.Services.Interfaces; +using HoneyBox.Model.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace HoneyBox.Admin.Business.Services; + +/// +/// 单页管理服务实现 +/// +public class DanyeService : IDanyeService +{ + private readonly HoneyBoxDbContext _dbContext; + private readonly ILogger _logger; + + /// + /// 标题不可编辑的ID范围(2-20) + /// + private const int TitleEditableMinId = 2; + private const int TitleEditableMaxId = 20; + + public DanyeService( + HoneyBoxDbContext dbContext, + ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + /// + public async Task> GetDanyeListAsync() + { + var danyes = await _dbContext.Danyes + .AsNoTracking() + .OrderBy(d => d.Id) + .ToListAsync(); + + return danyes.Select(d => new DanyeResponse + { + Id = d.Id, + Title = d.Title, + IsImageOptimizer = d.IsImageOptimizer == 1, + UpdateTime = UnixTimeToDateTime(d.UpdateTime) + }).ToList(); + } + + /// + public async Task GetDanyeByIdAsync(int id) + { + var danye = await _dbContext.Danyes + .AsNoTracking() + .FirstOrDefaultAsync(d => d.Id == id); + + if (danye == null) + return null; + + return new DanyeDetailResponse + { + Id = danye.Id, + Title = danye.Title, + Content = danye.Content, + IsImageOptimizer = danye.IsImageOptimizer == 1, + IsTitleEditable = !IsTitleProtected(danye.Id) + }; + } + + + /// + public async Task UpdateDanyeAsync(int id, DanyeUpdateRequest request) + { + var danye = await _dbContext.Danyes.FirstOrDefaultAsync(d => d.Id == id); + if (danye == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "单页不存在"); + } + + // 验证内容不能为空 + if (string.IsNullOrWhiteSpace(request.Content)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "内容不能为空"); + } + + // 更新标题(仅当ID不在2-20范围内且提供了标题时) + if (!string.IsNullOrWhiteSpace(request.Title)) + { + if (IsTitleProtected(id)) + { + _logger.LogWarning("尝试修改受保护的单页标题: Id={Id}", id); + // 不抛出异常,只是忽略标题更新 + } + else + { + danye.Title = request.Title; + } + } + + // 更新内容 + danye.Content = request.Content; + danye.UpdateTime = GetUnixTimestamp(); + + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("更新单页成功: Id={Id}", id); + + return result; + } + + /// + public async Task ToggleImageOptimizerAsync(int id, ImageOptimizerRequest request) + { + var danye = await _dbContext.Danyes.FirstOrDefaultAsync(d => d.Id == id); + if (danye == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "单页不存在"); + } + + danye.IsImageOptimizer = (byte)(request.IsImageOptimizer ? 1 : 0); + danye.UpdateTime = GetUnixTimestamp(); + + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("切换单页图片优化状态: Id={Id}, IsImageOptimizer={IsImageOptimizer}", + id, request.IsImageOptimizer); + + return result; + } + + #region Private Helper Methods + + /// + /// 判断标题是否受保护(ID 2-20 的单页标题不可编辑) + /// + private static bool IsTitleProtected(int id) + { + return id >= TitleEditableMinId && id <= TitleEditableMaxId; + } + + /// + /// Unix时间戳转DateTime + /// + private static DateTime UnixTimeToDateTime(int unixTime) + { + return DateTimeOffset.FromUnixTimeSeconds(unixTime).LocalDateTime; + } + + /// + /// 获取当前Unix时间戳 + /// + private static int GetUnixTimestamp() + { + return (int)DateTimeOffset.Now.ToUnixTimeSeconds(); + } + + #endregion +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Services/FloatBallService.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/FloatBallService.cs new file mode 100644 index 00000000..d8cd2b7f --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/FloatBallService.cs @@ -0,0 +1,277 @@ +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.FloatBall; +using HoneyBox.Admin.Business.Services.Interfaces; +using HoneyBox.Model.Data; +using HoneyBox.Model.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace HoneyBox.Admin.Business.Services; + +/// +/// 悬浮球配置服务实现 +/// +public class FloatBallService : IFloatBallService +{ + private readonly HoneyBoxDbContext _dbContext; + private readonly ILogger _logger; + + public FloatBallService( + HoneyBoxDbContext dbContext, + ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + /// + public async Task> GetFloatBallsAsync(FloatBallListRequest request) + { + var query = _dbContext.FloatBallConfigs.AsNoTracking(); + + var total = await query.CountAsync(); + + var items = await query + .OrderByDescending(f => f.CreatedAt) + .Skip(request.Skip) + .Take(request.PageSize) + .ToListAsync(); + + var list = items.Select(MapToResponse).ToList(); + + return PagedResult.Create(list, total, request.Page, request.PageSize); + } + + /// + public async Task GetFloatBallByIdAsync(int id) + { + var entity = await _dbContext.FloatBallConfigs + .AsNoTracking() + .FirstOrDefaultAsync(f => f.Id == id); + + return entity == null ? null : MapToResponse(entity); + } + + /// + public async Task CreateFloatBallAsync(FloatBallCreateRequest request) + { + // 验证必填字段 + ValidateCreateRequest(request); + + var entity = new FloatBallConfig + { + Title = request.Title, + Type = (byte)request.Type, + Image = request.Image, + ImageBj = request.ImageBj, + ImageDetails = request.ImageDetails, + LinkUrl = request.LinkUrl ?? string.Empty, + PositionX = request.PositionX, + PositionY = request.PositionY, + Width = request.Width, + Height = request.Height, + ImageDetailsX = request.ImageDetailsX, + ImageDetailsY = request.ImageDetailsY, + ImageDetailsW = request.ImageDetailsW, + ImageDetailsH = request.ImageDetailsH, + Effect = (byte)request.Effect, + Status = (byte)request.Status, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now + }; + + _dbContext.FloatBallConfigs.Add(entity); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation("创建悬浮球成功: Id={Id}, Title={Title}", entity.Id, entity.Title); + + return entity.Id; + } + + /// + public async Task UpdateFloatBallAsync(int id, FloatBallUpdateRequest request) + { + var entity = await _dbContext.FloatBallConfigs.FirstOrDefaultAsync(f => f.Id == id); + if (entity == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "悬浮球配置不存在"); + } + + // 验证必填字段 + ValidateUpdateRequest(request); + + entity.Title = request.Title; + entity.Type = (byte)request.Type; + entity.Image = request.Image; + entity.ImageBj = request.ImageBj; + entity.ImageDetails = request.ImageDetails; + entity.LinkUrl = request.LinkUrl ?? string.Empty; + entity.PositionX = request.PositionX; + entity.PositionY = request.PositionY; + entity.Width = request.Width; + entity.Height = request.Height; + entity.ImageDetailsX = request.ImageDetailsX; + entity.ImageDetailsY = request.ImageDetailsY; + entity.ImageDetailsW = request.ImageDetailsW; + entity.ImageDetailsH = request.ImageDetailsH; + entity.Effect = (byte)request.Effect; + entity.Status = (byte)request.Status; + entity.UpdatedAt = DateTime.Now; + + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("更新悬浮球成功: Id={Id}", id); + + return result; + } + + /// + public async Task DeleteFloatBallAsync(int id) + { + var entity = await _dbContext.FloatBallConfigs.FirstOrDefaultAsync(f => f.Id == id); + if (entity == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "悬浮球配置不存在"); + } + + _dbContext.FloatBallConfigs.Remove(entity); + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("删除悬浮球成功: Id={Id}", id); + + return result; + } + + /// + public async Task UpdateStatusAsync(int id, FloatBallStatusRequest request) + { + var entity = await _dbContext.FloatBallConfigs.FirstOrDefaultAsync(f => f.Id == id); + if (entity == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "悬浮球配置不存在"); + } + + entity.Status = (byte)request.Status; + entity.UpdatedAt = DateTime.Now; + + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("更新悬浮球状态: Id={Id}, Status={Status}", id, request.Status); + + return result; + } + + #region Private Helper Methods + + /// + /// 验证创建请求必填字段 + /// + private static void ValidateCreateRequest(FloatBallCreateRequest request) + { + if (request.Type < 1 || request.Type > 2) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "类型必须为1(展示图片)或2(跳转页面)"); + } + + if (string.IsNullOrWhiteSpace(request.Image)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "悬浮球图片不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.PositionX)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "X轴位置不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.PositionY)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "Y轴位置不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Width)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "宽度不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Height)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "高度不能为空"); + } + + if (request.Effect < 0 || request.Effect > 1) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "特效必须为0(无)或1(缩放动画)"); + } + } + + /// + /// 验证更新请求必填字段 + /// + private static void ValidateUpdateRequest(FloatBallUpdateRequest request) + { + if (request.Type < 1 || request.Type > 2) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "类型必须为1(展示图片)或2(跳转页面)"); + } + + if (string.IsNullOrWhiteSpace(request.Image)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "悬浮球图片不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.PositionX)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "X轴位置不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.PositionY)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "Y轴位置不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Width)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "宽度不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Height)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "高度不能为空"); + } + + if (request.Effect < 0 || request.Effect > 1) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "特效必须为0(无)或1(缩放动画)"); + } + } + + /// + /// 将实体映射为响应模型 + /// + private static FloatBallResponse MapToResponse(FloatBallConfig entity) + { + return new FloatBallResponse + { + Id = entity.Id, + Title = entity.Title, + Type = entity.Type, + Image = entity.Image, + ImageBj = entity.ImageBj, + ImageDetails = entity.ImageDetails, + LinkUrl = entity.LinkUrl, + PositionX = entity.PositionX, + PositionY = entity.PositionY, + Width = entity.Width, + Height = entity.Height, + ImageDetailsX = entity.ImageDetailsX, + ImageDetailsY = entity.ImageDetailsY, + ImageDetailsW = entity.ImageDetailsW, + ImageDetailsH = entity.ImageDetailsH, + Effect = entity.Effect, + Status = entity.Status, + CreatedAt = entity.CreatedAt + }; + } + + #endregion +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IDanyeService.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IDanyeService.cs new file mode 100644 index 00000000..a224c1c2 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IDanyeService.cs @@ -0,0 +1,38 @@ +using HoneyBox.Admin.Business.Models.Danye; + +namespace HoneyBox.Admin.Business.Services.Interfaces; + +/// +/// 单页管理服务接口 +/// +public interface IDanyeService +{ + /// + /// 获取单页列表 + /// + /// 单页列表 + Task> GetDanyeListAsync(); + + /// + /// 获取单页详情 + /// + /// 单页ID + /// 单页详情 + Task GetDanyeByIdAsync(int id); + + /// + /// 更新单页内容 + /// + /// 单页ID + /// 更新请求 + /// 是否成功 + Task UpdateDanyeAsync(int id, DanyeUpdateRequest request); + + /// + /// 切换图片优化状态 + /// + /// 单页ID + /// 图片优化请求 + /// 是否成功 + Task ToggleImageOptimizerAsync(int id, ImageOptimizerRequest request); +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IFloatBallService.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IFloatBallService.cs new file mode 100644 index 00000000..393f86ae --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IFloatBallService.cs @@ -0,0 +1,54 @@ +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.FloatBall; + +namespace HoneyBox.Admin.Business.Services.Interfaces; + +/// +/// 悬浮球配置服务接口 +/// +public interface IFloatBallService +{ + /// + /// 获取悬浮球列表(分页) + /// + /// 分页请求 + /// 分页结果 + Task> GetFloatBallsAsync(FloatBallListRequest request); + + /// + /// 获取悬浮球详情 + /// + /// 悬浮球ID + /// 悬浮球详情 + Task GetFloatBallByIdAsync(int id); + + /// + /// 创建悬浮球 + /// + /// 创建请求 + /// 新创建的悬浮球ID + Task CreateFloatBallAsync(FloatBallCreateRequest request); + + /// + /// 更新悬浮球 + /// + /// 悬浮球ID + /// 更新请求 + /// 是否成功 + Task UpdateFloatBallAsync(int id, FloatBallUpdateRequest request); + + /// + /// 删除悬浮球 + /// + /// 悬浮球ID + /// 是否成功 + Task DeleteFloatBallAsync(int id); + + /// + /// 更新悬浮球状态 + /// + /// 悬浮球ID + /// 状态请求 + /// 是否成功 + Task UpdateStatusAsync(int id, FloatBallStatusRequest request); +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IWelfareHouseService.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IWelfareHouseService.cs new file mode 100644 index 00000000..9332d811 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IWelfareHouseService.cs @@ -0,0 +1,54 @@ +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.WelfareHouse; + +namespace HoneyBox.Admin.Business.Services.Interfaces; + +/// +/// 福利屋入口服务接口 +/// +public interface IWelfareHouseService +{ + /// + /// 获取福利屋入口列表(分页) + /// + /// 分页请求 + /// 分页结果 + Task> GetWelfareHousesAsync(WelfareHouseListRequest request); + + /// + /// 获取福利屋入口详情 + /// + /// 福利屋入口ID + /// 福利屋入口详情 + Task GetWelfareHouseByIdAsync(int id); + + /// + /// 创建福利屋入口 + /// + /// 创建请求 + /// 新创建的福利屋入口ID + Task CreateWelfareHouseAsync(WelfareHouseCreateRequest request); + + /// + /// 更新福利屋入口 + /// + /// 福利屋入口ID + /// 更新请求 + /// 是否成功 + Task UpdateWelfareHouseAsync(int id, WelfareHouseUpdateRequest request); + + /// + /// 删除福利屋入口 + /// + /// 福利屋入口ID + /// 是否成功 + Task DeleteWelfareHouseAsync(int id); + + /// + /// 更新福利屋入口状态 + /// + /// 福利屋入口ID + /// 状态请求 + /// 是否成功 + Task UpdateStatusAsync(int id, WelfareHouseStatusRequest request); +} diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Services/WelfareHouseService.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/WelfareHouseService.cs new file mode 100644 index 00000000..94981c59 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Services/WelfareHouseService.cs @@ -0,0 +1,208 @@ +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.WelfareHouse; +using HoneyBox.Admin.Business.Services.Interfaces; +using HoneyBox.Model.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace HoneyBox.Admin.Business.Services; + +/// +/// 福利屋入口服务实现 +/// +public class WelfareHouseService : IWelfareHouseService +{ + private readonly HoneyBoxDbContext _dbContext; + private readonly ILogger _logger; + + public WelfareHouseService( + HoneyBoxDbContext dbContext, + ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + /// + public async Task> GetWelfareHousesAsync(WelfareHouseListRequest request) + { + var query = _dbContext.WelfareHouses.AsNoTracking(); + + var total = await query.CountAsync(); + + var items = await query + .OrderBy(w => w.Sort) + .ThenByDescending(w => w.CreateTime) + .Skip(request.Skip) + .Take(request.PageSize) + .ToListAsync(); + + var list = items.Select(MapToResponse).ToList(); + + return PagedResult.Create(list, total, request.Page, request.PageSize); + } + + /// + public async Task GetWelfareHouseByIdAsync(int id) + { + var entity = await _dbContext.WelfareHouses + .AsNoTracking() + .FirstOrDefaultAsync(w => w.Id == id); + + return entity == null ? null : MapToResponse(entity); + } + + + /// + public async Task CreateWelfareHouseAsync(WelfareHouseCreateRequest request) + { + // 验证必填字段 + ValidateCreateRequest(request); + + var now = (int)DateTimeOffset.Now.ToUnixTimeSeconds(); + var entity = new Model.Entities.WelfareHouse + { + Name = request.Name, + Image = request.Image, + Url = request.Url, + Sort = request.Sort, + Status = (byte)request.Status, + CreateTime = now, + UpdateTime = now + }; + + _dbContext.WelfareHouses.Add(entity); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation("创建福利屋入口成功: Id={Id}, Name={Name}", entity.Id, entity.Name); + + return entity.Id; + } + + /// + public async Task UpdateWelfareHouseAsync(int id, WelfareHouseUpdateRequest request) + { + var entity = await _dbContext.WelfareHouses.FirstOrDefaultAsync(w => w.Id == id); + if (entity == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "福利屋入口不存在"); + } + + // 验证必填字段 + ValidateUpdateRequest(request); + + entity.Name = request.Name; + entity.Image = request.Image; + entity.Url = request.Url; + entity.Sort = request.Sort; + entity.Status = (byte)request.Status; + entity.UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(); + + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("更新福利屋入口成功: Id={Id}", id); + + return result; + } + + /// + public async Task DeleteWelfareHouseAsync(int id) + { + var entity = await _dbContext.WelfareHouses.FirstOrDefaultAsync(w => w.Id == id); + if (entity == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "福利屋入口不存在"); + } + + _dbContext.WelfareHouses.Remove(entity); + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("删除福利屋入口成功: Id={Id}", id); + + return result; + } + + /// + public async Task UpdateStatusAsync(int id, WelfareHouseStatusRequest request) + { + var entity = await _dbContext.WelfareHouses.FirstOrDefaultAsync(w => w.Id == id); + if (entity == null) + { + throw new BusinessException(BusinessErrorCodes.NotFound, "福利屋入口不存在"); + } + + entity.Status = (byte)request.Status; + entity.UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(); + + var result = await _dbContext.SaveChangesAsync() > 0; + + _logger.LogInformation("更新福利屋入口状态: Id={Id}, Status={Status}", id, request.Status); + + return result; + } + + #region Private Helper Methods + + /// + /// 验证创建请求必填字段 + /// + private static void ValidateCreateRequest(WelfareHouseCreateRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "名称不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Image)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "图片不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Url)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "跳转链接不能为空"); + } + } + + /// + /// 验证更新请求必填字段 + /// + private static void ValidateUpdateRequest(WelfareHouseUpdateRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "名称不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Image)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "图片不能为空"); + } + + if (string.IsNullOrWhiteSpace(request.Url)) + { + throw new BusinessException(BusinessErrorCodes.ValidationFailed, "跳转链接不能为空"); + } + } + + /// + /// 将实体映射为响应模型 + /// + private static WelfareHouseResponse MapToResponse(Model.Entities.WelfareHouse entity) + { + return new WelfareHouseResponse + { + Id = entity.Id, + Name = entity.Name, + Image = entity.Image, + Url = entity.Url, + Sort = entity.Sort, + Status = entity.Status, + CreateTime = entity.CreateTime.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(entity.CreateTime.Value).LocalDateTime + : null + }; + } + + #endregion +} diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/danye.ts b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/danye.ts new file mode 100644 index 00000000..04079735 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/danye.ts @@ -0,0 +1,100 @@ +import { request, type ApiResponse } from '@/utils/request' + +// ==================== 单页管理类型定义 ==================== + +/** 单页列表响应 */ +export interface DanyeResponse { + /** 单页ID */ + id: number + /** 标题 */ + title: string + /** 是否启用图片优化 */ + isImageOptimizer: boolean + /** 更新时间 */ + updateTime: string +} + +/** 单页详情响应 */ +export interface DanyeDetailResponse { + /** 单页ID */ + id: number + /** 标题 */ + title: string + /** 内容(富文本HTML) */ + content: string + /** 是否启用图片优化 */ + isImageOptimizer: boolean + /** 标题是否可编辑(ID 2-20 不可编辑) */ + isTitleEditable: boolean +} + +/** 单页更新请求 */ +export interface DanyeUpdateRequest { + /** 标题(可选,ID 2-20 的单页标题不可编辑) */ + title?: string + /** 内容(富文本HTML) */ + content: string +} + +/** 图片优化切换请求 */ +export interface ImageOptimizerRequest { + /** 是否启用图片优化 */ + isImageOptimizer: boolean +} + +// ==================== API 基础路径 ==================== + +const DANYE_BASE_URL = '/admin/business/danye' + +// ==================== 单页管理 API ==================== + +/** + * 获取单页列表 + * @returns 单页列表 + */ +export function getDanyeList(): Promise> { + return request({ + url: DANYE_BASE_URL, + method: 'get' + }) +} + +/** + * 获取单页详情 + * @param id 单页ID + * @returns 单页详情 + */ +export function getDanyeById(id: number): Promise> { + return request({ + url: `${DANYE_BASE_URL}/${id}`, + method: 'get' + }) +} + +/** + * 更新单页内容 + * @param id 单页ID + * @param data 更新请求数据 + * @returns 操作结果 + */ +export function updateDanye(id: number, data: DanyeUpdateRequest): Promise> { + return request({ + url: `${DANYE_BASE_URL}/${id}`, + method: 'put', + data + }) +} + +/** + * 切换图片优化状态 + * @param id 单页ID + * @param data 图片优化请求数据 + * @returns 操作结果 + */ +export function toggleImageOptimizer(id: number, data: ImageOptimizerRequest): Promise> { + return request({ + url: `${DANYE_BASE_URL}/${id}/image-optimizer`, + method: 'put', + data + }) +} diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/floatball.ts b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/floatball.ts new file mode 100644 index 00000000..bae7a32e --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/floatball.ts @@ -0,0 +1,243 @@ +import { request, type ApiResponse, type PagedResult } from '@/utils/request' + +// ==================== 悬浮球类型枚举 ==================== + +/** 悬浮球类型枚举 */ +export enum FloatBallType { + /** 展示图片 */ + ShowImage = 1, + /** 跳转页面 */ + JumpPage = 2 +} + +/** 悬浮球类型标签映射 */ +export const FloatBallTypeLabels: Record = { + [FloatBallType.ShowImage]: '展示图片', + [FloatBallType.JumpPage]: '跳转页面' +} + +/** 悬浮球特效枚举 */ +export enum FloatBallEffect { + /** 无特效 */ + None = 0, + /** 缩放动画 */ + Scale = 1 +} + +/** 悬浮球特效标签映射 */ +export const FloatBallEffectLabels: Record = { + [FloatBallEffect.None]: '无特效', + [FloatBallEffect.Scale]: '缩放动画' +} + +// ==================== 悬浮球类型定义 ==================== + +/** 悬浮球列表查询请求 */ +export interface FloatBallListRequest { + /** 页码 */ + page: number + /** 每页数量 */ + pageSize: number +} + +/** 悬浮球响应 */ +export interface FloatBallResponse { + /** 主键ID */ + id: number + /** 标题 */ + title?: string + /** 类型: 1展示图片 2跳转页面 */ + type: number + /** 悬浮球图片URL */ + image: string + /** 背景图片URL */ + imageBj?: string + /** 详情图片URL */ + imageDetails?: string + /** 跳转链接 */ + linkUrl: string + /** X轴位置 */ + positionX: string + /** Y轴位置 */ + positionY: string + /** 宽度 */ + width: string + /** 高度 */ + height: string + /** 详情图片X偏移 */ + imageDetailsX?: string + /** 详情图片Y偏移 */ + imageDetailsY?: string + /** 详情图片宽度 */ + imageDetailsW?: string + /** 详情图片高度 */ + imageDetailsH?: string + /** 特效: 0无 1缩放动画 */ + effect: number + /** 状态: 0关闭 1开启 */ + status: number + /** 创建时间 */ + createdAt: string +} + +/** 悬浮球创建请求 */ +export interface FloatBallCreateRequest { + /** 标题 */ + title?: string + /** 类型: 1展示图片 2跳转页面 */ + type: number + /** 悬浮球图片URL(必填) */ + image: string + /** 背景图片URL */ + imageBj?: string + /** 详情图片URL */ + imageDetails?: string + /** 跳转链接(类型为跳转页面时使用) */ + linkUrl?: string + /** X轴位置(必填) */ + positionX: string + /** Y轴位置(必填) */ + positionY: string + /** 宽度(必填) */ + width: string + /** 高度(必填) */ + height: string + /** 详情图片X偏移 */ + imageDetailsX?: string + /** 详情图片Y偏移 */ + imageDetailsY?: string + /** 详情图片宽度 */ + imageDetailsW?: string + /** 详情图片高度 */ + imageDetailsH?: string + /** 特效: 0无 1缩放动画(必填) */ + effect: number + /** 状态: 0关闭 1开启,默认开启 */ + status?: number +} + +/** 悬浮球更新请求 */ +export interface FloatBallUpdateRequest { + /** 标题 */ + title?: string + /** 类型: 1展示图片 2跳转页面 */ + type: number + /** 悬浮球图片URL(必填) */ + image: string + /** 背景图片URL */ + imageBj?: string + /** 详情图片URL */ + imageDetails?: string + /** 跳转链接(类型为跳转页面时使用) */ + linkUrl?: string + /** X轴位置(必填) */ + positionX: string + /** Y轴位置(必填) */ + positionY: string + /** 宽度(必填) */ + width: string + /** 高度(必填) */ + height: string + /** 详情图片X偏移 */ + imageDetailsX?: string + /** 详情图片Y偏移 */ + imageDetailsY?: string + /** 详情图片宽度 */ + imageDetailsW?: string + /** 详情图片高度 */ + imageDetailsH?: string + /** 特效: 0无 1缩放动画(必填) */ + effect: number + /** 状态: 0关闭 1开启 */ + status: number +} + +/** 悬浮球状态切换请求 */ +export interface FloatBallStatusRequest { + /** 状态: 0关闭 1开启 */ + status: number +} + +// ==================== API 基础路径 ==================== + +const FLOATBALL_BASE_URL = '/admin/business/floatball' + +// ==================== 悬浮球配置 API ==================== + +/** + * 获取悬浮球列表(分页) + * @param params 查询参数 + * @returns 分页悬浮球列表 + */ +export function getFloatBalls(params: FloatBallListRequest): Promise>> { + return request({ + url: FLOATBALL_BASE_URL, + method: 'get', + params + }) +} + +/** + * 获取悬浮球详情 + * @param id 悬浮球ID + * @returns 悬浮球详情 + */ +export function getFloatBallById(id: number): Promise> { + return request({ + url: `${FLOATBALL_BASE_URL}/${id}`, + method: 'get' + }) +} + +/** + * 创建悬浮球 + * @param data 创建请求数据 + * @returns 新悬浮球ID + */ +export function createFloatBall(data: FloatBallCreateRequest): Promise> { + return request({ + url: FLOATBALL_BASE_URL, + method: 'post', + data + }) +} + +/** + * 更新悬浮球 + * @param id 悬浮球ID + * @param data 更新请求数据 + * @returns 操作结果 + */ +export function updateFloatBall(id: number, data: FloatBallUpdateRequest): Promise> { + return request({ + url: `${FLOATBALL_BASE_URL}/${id}`, + method: 'put', + data + }) +} + +/** + * 删除悬浮球 + * @param id 悬浮球ID + * @returns 操作结果 + */ +export function deleteFloatBall(id: number): Promise> { + return request({ + url: `${FLOATBALL_BASE_URL}/${id}`, + method: 'delete' + }) +} + +/** + * 切换悬浮球状态 + * @param id 悬浮球ID + * @param data 状态请求数据 + * @returns 操作结果 + */ +export function updateFloatBallStatus(id: number, data: FloatBallStatusRequest): Promise> { + return request({ + url: `${FLOATBALL_BASE_URL}/${id}/status`, + method: 'put', + data + }) +} diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/welfarehouse.ts b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/welfarehouse.ts new file mode 100644 index 00000000..bb9c8b6c --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/welfarehouse.ts @@ -0,0 +1,147 @@ +import { request, type ApiResponse, type PagedResult } from '@/utils/request' + +// ==================== 福利屋入口类型定义 ==================== + +/** 福利屋入口列表查询请求 */ +export interface WelfareHouseListRequest { + /** 页码 */ + page: number + /** 每页数量 */ + pageSize: number +} + +/** 福利屋入口响应 */ +export interface WelfareHouseResponse { + /** 主键ID */ + id: number + /** 名称 */ + name: string + /** 图片URL */ + image: string + /** 跳转链接 */ + url: string + /** 排序值 */ + sort: number + /** 状态: 0禁用 1启用 */ + status: number + /** 创建时间 */ + createTime?: string +} + +/** 福利屋入口创建请求 */ +export interface WelfareHouseCreateRequest { + /** 名称(必填) */ + name: string + /** 图片URL(必填) */ + image: string + /** 跳转链接(必填) */ + url: string + /** 排序值(必填) */ + sort: number + /** 状态: 0禁用 1启用,默认启用 */ + status?: number +} + +/** 福利屋入口更新请求 */ +export interface WelfareHouseUpdateRequest { + /** 名称(必填) */ + name: string + /** 图片URL(必填) */ + image: string + /** 跳转链接(必填) */ + url: string + /** 排序值(必填) */ + sort: number + /** 状态: 0禁用 1启用 */ + status: number +} + +/** 福利屋入口状态切换请求 */ +export interface WelfareHouseStatusRequest { + /** 状态: 0禁用 1启用 */ + status: number +} + +// ==================== API 基础路径 ==================== + +const WELFAREHOUSE_BASE_URL = '/admin/business/welfarehouse' + +// ==================== 福利屋入口 API ==================== + +/** + * 获取福利屋入口列表(分页) + * @param params 查询参数 + * @returns 分页福利屋入口列表 + */ +export function getWelfareHouses(params: WelfareHouseListRequest): Promise>> { + return request({ + url: WELFAREHOUSE_BASE_URL, + method: 'get', + params + }) +} + +/** + * 获取福利屋入口详情 + * @param id 福利屋入口ID + * @returns 福利屋入口详情 + */ +export function getWelfareHouseById(id: number): Promise> { + return request({ + url: `${WELFAREHOUSE_BASE_URL}/${id}`, + method: 'get' + }) +} + +/** + * 创建福利屋入口 + * @param data 创建请求数据 + * @returns 新福利屋入口ID + */ +export function createWelfareHouse(data: WelfareHouseCreateRequest): Promise> { + return request({ + url: WELFAREHOUSE_BASE_URL, + method: 'post', + data + }) +} + +/** + * 更新福利屋入口 + * @param id 福利屋入口ID + * @param data 更新请求数据 + * @returns 操作结果 + */ +export function updateWelfareHouse(id: number, data: WelfareHouseUpdateRequest): Promise> { + return request({ + url: `${WELFAREHOUSE_BASE_URL}/${id}`, + method: 'put', + data + }) +} + +/** + * 删除福利屋入口 + * @param id 福利屋入口ID + * @returns 操作结果 + */ +export function deleteWelfareHouse(id: number): Promise> { + return request({ + url: `${WELFAREHOUSE_BASE_URL}/${id}`, + method: 'delete' + }) +} + +/** + * 切换福利屋入口状态 + * @param id 福利屋入口ID + * @param data 状态请求数据 + * @returns 操作结果 + */ +export function updateWelfareHouseStatus(id: number, data: WelfareHouseStatusRequest): Promise> { + return request({ + url: `${WELFAREHOUSE_BASE_URL}/${id}/status`, + method: 'put', + data + }) +} diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/router/modules/business.ts b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/router/modules/business.ts index fc77e514..ff84ac4b 100644 --- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/router/modules/business.ts +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/router/modules/business.ts @@ -458,6 +458,47 @@ export const businessRoutes: RouteRecordRaw[] = [ } } ] + }, + // 内容管理 + { + path: 'content', + redirect: '/business/danye/list', + meta: { + title: '内容管理', + icon: 'Document' + }, + children: [ + { + path: '/business/danye/list', + name: 'DanyeList', + component: () => import('@/views/business/danye/list.vue'), + meta: { + title: '单页管理', + permission: 'danye:list', + keepAlive: true + } + }, + { + path: '/business/floatball/list', + name: 'FloatBallList', + component: () => import('@/views/business/floatball/list.vue'), + meta: { + title: '悬浮球配置', + permission: 'floatball:list', + keepAlive: true + } + }, + { + path: '/business/welfarehouse/list', + name: 'WelfareHouseList', + component: () => import('@/views/business/welfarehouse/list.vue'), + meta: { + title: '福利屋入口', + permission: 'welfarehouse:list', + keepAlive: true + } + } + ] } ] } @@ -884,4 +925,63 @@ export const rankPermissions = { delete: 'rank:delete' } +/** + * 内容管理模块路由配置参考 + * + * 后端菜单配置示例: + * + * 1. 内容管理(目录) + * - name: 内容管理 + * - path: /business/content + * - menuType: 1 (目录) + * - icon: Document + * - sortOrder: 70 + * + * 2. 单页管理(菜单) + * - name: 单页管理 + * - path: /business/danye/list + * - component: business/danye/list + * - menuType: 2 (菜单) + * - permission: danye:list + * - sortOrder: 1 + * + * 3. 悬浮球配置(菜单) + * - name: 悬浮球配置 + * - path: /business/floatball/list + * - component: business/floatball/list + * - menuType: 2 (菜单) + * - permission: floatball:list + * - sortOrder: 2 + * + * 4. 福利屋入口(菜单) + * - name: 福利屋入口 + * - path: /business/welfarehouse/list + * - component: business/welfarehouse/list + * - menuType: 2 (菜单) + * - permission: welfarehouse:list + * - sortOrder: 3 + */ + +/** + * 内容管理模块权限配置 + * 用于按钮级别权限控制 + */ +export const contentPermissions = { + // 单页管理 + danyeList: 'danye:list', + danyeEdit: 'danye:edit', + + // 悬浮球配置 + floatballList: 'floatball:list', + floatballAdd: 'floatball:add', + floatballEdit: 'floatball:edit', + floatballDelete: 'floatball:delete', + + // 福利屋入口 + welfareHouseList: 'welfarehouse:list', + welfareHouseAdd: 'welfarehouse:add', + welfareHouseEdit: 'welfarehouse:edit', + welfareHouseDelete: 'welfarehouse:delete' +} + export default businessRoutes diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeFormDialog.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeFormDialog.vue new file mode 100644 index 00000000..bae7b24f --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeFormDialog.vue @@ -0,0 +1,351 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeTable.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeTable.vue new file mode 100644 index 00000000..93f24867 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeTable.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/list.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/list.vue new file mode 100644 index 00000000..f5a2586e --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/list.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallFormDialog.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallFormDialog.vue new file mode 100644 index 00000000..e778384a --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallFormDialog.vue @@ -0,0 +1,478 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallTable.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallTable.vue new file mode 100644 index 00000000..e4584000 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallTable.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/list.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/list.vue new file mode 100644 index 00000000..a6a8dc2e --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/list.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseFormDialog.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseFormDialog.vue new file mode 100644 index 00000000..65ab1ded --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseFormDialog.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseTable.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseTable.vue new file mode 100644 index 00000000..7b8b3aac --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseTable.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/list.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/list.vue new file mode 100644 index 00000000..bfe91e7c --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/list.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/server/HoneyBox/tests/HoneyBox.Tests/Services/ContentAuxiliaryFrontendPropertyTests.cs b/server/HoneyBox/tests/HoneyBox.Tests/Services/ContentAuxiliaryFrontendPropertyTests.cs new file mode 100644 index 00000000..091835aa --- /dev/null +++ b/server/HoneyBox/tests/HoneyBox.Tests/Services/ContentAuxiliaryFrontendPropertyTests.cs @@ -0,0 +1,1371 @@ +using FsCheck; +using FsCheck.Xunit; +using HoneyBox.Admin.Business.Models; +using HoneyBox.Admin.Business.Models.FloatBall; +using HoneyBox.Admin.Business.Models.WelfareHouse; +using HoneyBox.Admin.Business.Services; +using HoneyBox.Model.Data; +using HoneyBox.Model.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace HoneyBox.Tests.Services; + +/// +/// 内容与辅助模块前端属性测试 +/// Feature: content-auxiliary-frontend +/// +public class ContentAuxiliaryFrontendPropertyTests +{ + private readonly Mock> _mockFloatBallLogger = new(); + private readonly Mock> _mockWelfareHouseLogger = new(); + + #region Property 1: 分页参数正确传递 + + /// + /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** + /// For any pagination request to FloatBall list, the returned list should have at most pageSize items, + /// and the page and pageSize in response should match the request. + /// **Validates: Requirements 3.4** + /// + [Property(MaxTest = 100)] + public bool FloatBallPagination_ShouldReturnCorrectPageSize(PositiveInt seed) + { + var itemCount = (seed.Get % 30) + 10; // 10 to 39 items + var pageSize = (seed.Get % 10) + 1; // 1 to 10 per page + var page = (seed.Get % 5) + 1; // page 1 to 5 + + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Create test float balls + for (int i = 0; i < itemCount; i++) + { + dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); + } + dbContext.SaveChanges(); + + var request = new FloatBallListRequest { Page = page, PageSize = pageSize }; + var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); + + // Verify pagination parameters are correctly passed + return result.Total == itemCount && + result.List.Count <= pageSize && + result.Page == page && + result.PageSize == pageSize; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** + /// For any pagination request to WelfareHouse list, the returned list should have at most pageSize items, + /// and the page and pageSize in response should match the request. + /// **Validates: Requirements 7.4** + /// + [Property(MaxTest = 100)] + public bool WelfareHousePagination_ShouldReturnCorrectPageSize(PositiveInt seed) + { + var itemCount = (seed.Get % 30) + 10; // 10 to 39 items + var pageSize = (seed.Get % 10) + 1; // 1 to 10 per page + var page = (seed.Get % 5) + 1; // page 1 to 5 + + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + // Create test welfare house entries + for (int i = 0; i < itemCount; i++) + { + dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); + } + dbContext.SaveChanges(); + + var request = new WelfareHouseListRequest { Page = page, PageSize = pageSize }; + var result = service.GetWelfareHousesAsync(request).GetAwaiter().GetResult(); + + // Verify pagination parameters are correctly passed + return result.Total == itemCount && + result.List.Count <= pageSize && + result.Page == page && + result.PageSize == pageSize; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** + /// The total count should remain consistent regardless of which page is requested for FloatBall. + /// **Validates: Requirements 3.4** + /// + [Property(MaxTest = 100)] + public bool FloatBallPagination_TotalShouldBeConsistentAcrossPages(PositiveInt seed) + { + var itemCount = (seed.Get % 20) + 15; // 15 to 34 items + var pageSize = 5; + + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Create test float balls + for (int i = 0; i < itemCount; i++) + { + dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); + } + dbContext.SaveChanges(); + + // Get multiple pages + var page1 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); + var page2 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); + var page3 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 3, PageSize = pageSize }).GetAwaiter().GetResult(); + + // Total should be consistent across all pages + return page1.Total == page2.Total && + page2.Total == page3.Total && + page1.Total == itemCount; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** + /// The total count should remain consistent regardless of which page is requested for WelfareHouse. + /// **Validates: Requirements 7.4** + /// + [Property(MaxTest = 100)] + public bool WelfareHousePagination_TotalShouldBeConsistentAcrossPages(PositiveInt seed) + { + var itemCount = (seed.Get % 20) + 15; // 15 to 34 items + var pageSize = 5; + + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + // Create test welfare house entries + for (int i = 0; i < itemCount; i++) + { + dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); + } + dbContext.SaveChanges(); + + // Get multiple pages + var page1 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); + var page2 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); + var page3 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 3, PageSize = pageSize }).GetAwaiter().GetResult(); + + // Total should be consistent across all pages + return page1.Total == page2.Total && + page2.Total == page3.Total && + page1.Total == itemCount; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** + /// Different pages should return different items (no overlap) for FloatBall. + /// **Validates: Requirements 3.4** + /// + [Property(MaxTest = 100)] + public bool FloatBallPagination_DifferentPagesShouldNotOverlap(PositiveInt seed) + { + var itemCount = (seed.Get % 15) + 20; // 20 to 34 items + var pageSize = 5; + + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Create test float balls + for (int i = 0; i < itemCount; i++) + { + dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); + } + dbContext.SaveChanges(); + + // Get first two pages + var page1 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); + var page2 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); + + // IDs should not overlap between pages + var page1Ids = page1.List.Select(f => f.Id).ToHashSet(); + var page2Ids = page2.List.Select(f => f.Id).ToHashSet(); + + return !page1Ids.Overlaps(page2Ids); + } + + /// + /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** + /// Different pages should return different items (no overlap) for WelfareHouse. + /// **Validates: Requirements 7.4** + /// + [Property(MaxTest = 100)] + public bool WelfareHousePagination_DifferentPagesShouldNotOverlap(PositiveInt seed) + { + var itemCount = (seed.Get % 15) + 20; // 20 to 34 items + var pageSize = 5; + + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + // Create test welfare house entries + for (int i = 0; i < itemCount; i++) + { + dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); + } + dbContext.SaveChanges(); + + // Get first two pages + var page1 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); + var page2 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); + + // IDs should not overlap between pages + var page1Ids = page1.List.Select(w => w.Id).ToHashSet(); + var page2Ids = page2.List.Select(w => w.Id).ToHashSet(); + + return !page1Ids.Overlaps(page2Ids); + } + + #endregion + + #region Helper Methods + + private HoneyBoxDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + return new HoneyBoxDbContext(options); + } + + private FloatBallConfig CreateTestFloatBall(string title) + { + return new FloatBallConfig + { + Title = title, + Type = 1, + Image = "http://test.com/floatball.jpg", + LinkUrl = string.Empty, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now + }; + } + + private WelfareHouse CreateTestWelfareHouse(string name, int sort) + { + return new WelfareHouse + { + Name = name, + Image = "http://test.com/welfare.jpg", + Url = "/welfare/test", + Sort = sort, + Status = 1, + CreateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), + UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds() + }; + } + + #endregion +} + + +/// +/// 内容与辅助模块前端属性测试 - 第二部分 +/// Feature: content-auxiliary-frontend +/// +public class ContentAuxiliaryFrontendPropertyTests_Part2 +{ + private readonly Mock> _mockFloatBallLogger = new(); + private readonly Mock> _mockWelfareHouseLogger = new(); + + #region Property 2: 表单必填字段验证 + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When FloatBall type is invalid (not 1 or 2), the system should reject the creation. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithInvalidType_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Use invalid type values + var invalidTypes = new[] { 0, 3, 4, 5, -1, 100 }; + var invalidType = invalidTypes[seed.Get % invalidTypes.Length]; + + var request = new FloatBallCreateRequest + { + Title = "Test FloatBall", + Type = invalidType, + Image = "http://test.com/img.jpg", + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + try + { + service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("类型必须为1(展示图片)或2(跳转页面)"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When FloatBall image is empty, the system should reject the creation. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithEmptyImage_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var emptyImages = new[] { "", " ", null }; + var emptyImage = emptyImages[seed.Get % emptyImages.Length]; + + var request = new FloatBallCreateRequest + { + Title = "Test FloatBall", + Type = 1, + Image = emptyImage ?? string.Empty, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + try + { + service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("悬浮球图片不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When FloatBall position X is empty, the system should reject the creation. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithEmptyPositionX_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var emptyValues = new[] { "", " " }; + var emptyValue = emptyValues[seed.Get % emptyValues.Length]; + + var request = new FloatBallCreateRequest + { + Title = "Test FloatBall", + Type = 1, + Image = "http://test.com/img.jpg", + PositionX = emptyValue, + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + try + { + service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("X轴位置不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When FloatBall position Y is empty, the system should reject the creation. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithEmptyPositionY_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var emptyValues = new[] { "", " " }; + var emptyValue = emptyValues[seed.Get % emptyValues.Length]; + + var request = new FloatBallCreateRequest + { + Title = "Test FloatBall", + Type = 1, + Image = "http://test.com/img.jpg", + PositionX = "10", + PositionY = emptyValue, + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + try + { + service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("Y轴位置不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When FloatBall width is empty, the system should reject the creation. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithEmptyWidth_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var emptyValues = new[] { "", " " }; + var emptyValue = emptyValues[seed.Get % emptyValues.Length]; + + var request = new FloatBallCreateRequest + { + Title = "Test FloatBall", + Type = 1, + Image = "http://test.com/img.jpg", + PositionX = "10", + PositionY = "20", + Width = emptyValue, + Height = "50", + Effect = 0, + Status = 1 + }; + + try + { + service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("宽度不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When FloatBall height is empty, the system should reject the creation. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithEmptyHeight_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var emptyValues = new[] { "", " " }; + var emptyValue = emptyValues[seed.Get % emptyValues.Length]; + + var request = new FloatBallCreateRequest + { + Title = "Test FloatBall", + Type = 1, + Image = "http://test.com/img.jpg", + PositionX = "10", + PositionY = "20", + Width = "50", + Height = emptyValue, + Effect = 0, + Status = 1 + }; + + try + { + service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("高度不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When FloatBall effect is invalid (not 0 or 1), the system should reject the creation. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithInvalidEffect_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Use invalid effect values + var invalidEffects = new[] { 2, 3, -1, 100 }; + var invalidEffect = invalidEffects[seed.Get % invalidEffects.Length]; + + var request = new FloatBallCreateRequest + { + Title = "Test FloatBall", + Type = 1, + Image = "http://test.com/img.jpg", + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = invalidEffect, + Status = 1 + }; + + try + { + service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("特效必须为0(无)或1(缩放动画)"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When WelfareHouse name is empty, the system should reject the creation. + /// **Validates: Requirements 8.2** + /// + [Property(MaxTest = 100)] + public bool WelfareHouseCreate_WithEmptyName_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + var emptyNames = new[] { "", " " }; + var emptyName = emptyNames[seed.Get % emptyNames.Length]; + + var request = new WelfareHouseCreateRequest + { + Name = emptyName, + Image = "http://test.com/img.jpg", + Url = "/welfare/test", + Sort = 1, + Status = 1 + }; + + try + { + service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("名称不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When WelfareHouse image is empty, the system should reject the creation. + /// **Validates: Requirements 8.2** + /// + [Property(MaxTest = 100)] + public bool WelfareHouseCreate_WithEmptyImage_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + var emptyImages = new[] { "", " " }; + var emptyImage = emptyImages[seed.Get % emptyImages.Length]; + + var request = new WelfareHouseCreateRequest + { + Name = "Test WelfareHouse", + Image = emptyImage, + Url = "/welfare/test", + Sort = 1, + Status = 1 + }; + + try + { + service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("图片不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When WelfareHouse URL is empty, the system should reject the creation. + /// **Validates: Requirements 8.2** + /// + [Property(MaxTest = 100)] + public bool WelfareHouseCreate_WithEmptyUrl_ShouldFail(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + var emptyUrls = new[] { "", " " }; + var emptyUrl = emptyUrls[seed.Get % emptyUrls.Length]; + + var request = new WelfareHouseCreateRequest + { + Name = "Test WelfareHouse", + Image = "http://test.com/img.jpg", + Url = emptyUrl, + Sort = 1, + Status = 1 + }; + + try + { + service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); + return false; // Should have thrown exception + } + catch (BusinessException ex) + { + return ex.Message.Contains("跳转链接不能为空"); + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When all required fields are valid, FloatBall creation should succeed. + /// **Validates: Requirements 4.2** + /// + [Property(MaxTest = 100)] + public bool FloatBallCreate_WithValidData_ShouldSucceed(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var validTypes = new[] { 1, 2 }; + var validEffects = new[] { 0, 1 }; + + var request = new FloatBallCreateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = validTypes[seed.Get % validTypes.Length], + Image = "http://test.com/img.jpg", + PositionX = (seed.Get % 100).ToString(), + PositionY = (seed.Get % 100).ToString(), + Width = ((seed.Get % 50) + 20).ToString(), + Height = ((seed.Get % 50) + 20).ToString(), + Effect = validEffects[seed.Get % validEffects.Length], + Status = 1 + }; + + try + { + var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + var created = dbContext.FloatBallConfigs.Find(id); + return created != null && + created.Type == request.Type && + created.Image == request.Image && + created.PositionX == request.PositionX && + created.PositionY == request.PositionY && + created.Width == request.Width && + created.Height == request.Height && + created.Effect == request.Effect; + } + catch + { + return false; + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** + /// When all required fields are valid, WelfareHouse creation should succeed. + /// **Validates: Requirements 8.2** + /// + [Property(MaxTest = 100)] + public bool WelfareHouseCreate_WithValidData_ShouldSucceed(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + var request = new WelfareHouseCreateRequest + { + Name = $"Test WelfareHouse {seed.Get}", + Image = "http://test.com/img.jpg", + Url = $"/welfare/test{seed.Get}", + Sort = seed.Get % 100, + Status = 1 + }; + + try + { + var id = service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); + var created = dbContext.WelfareHouses.Find(id); + return created != null && + created.Name == request.Name && + created.Image == request.Image && + created.Url == request.Url && + created.Sort == request.Sort; + } + catch + { + return false; + } + } + + #endregion + + #region Helper Methods + + private HoneyBoxDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + return new HoneyBoxDbContext(options); + } + + #endregion +} + + +/// +/// 内容与辅助模块前端属性测试 - 第三部分 +/// Feature: content-auxiliary-frontend +/// +public class ContentAuxiliaryFrontendPropertyTests_Part3 +{ + private readonly Mock> _mockFloatBallLogger = new(); + + #region Property 3: 条件显示字段正确切换 + + /// + /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** + /// When FloatBall type is 1 (展示图片), the LinkUrl field should be optional and can be empty. + /// **Validates: Requirements 4.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallType1_LinkUrlShouldBeOptional(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Type 1 = 展示图片, LinkUrl should be optional + var request = new FloatBallCreateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = 1, // 展示图片 + Image = "http://test.com/img.jpg", + LinkUrl = null, // Empty link URL + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + try + { + var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + var created = dbContext.FloatBallConfigs.Find(id); + // For type 1, creation should succeed even without LinkUrl + return created != null && created.Type == 1; + } + catch + { + return false; + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** + /// When FloatBall type is 2 (跳转页面), the LinkUrl field can be provided for navigation. + /// **Validates: Requirements 4.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallType2_LinkUrlShouldBeUsed(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var linkUrl = $"/page/test{seed.Get}"; + + // Type 2 = 跳转页面, LinkUrl should be used + var request = new FloatBallCreateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = 2, // 跳转页面 + Image = "http://test.com/img.jpg", + LinkUrl = linkUrl, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + try + { + var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + var created = dbContext.FloatBallConfigs.Find(id); + // For type 2, LinkUrl should be stored correctly + return created != null && + created.Type == 2 && + created.LinkUrl == linkUrl; + } + catch + { + return false; + } + } + + /// + /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** + /// When FloatBall type changes from 1 to 2, the LinkUrl should be updatable. + /// **Validates: Requirements 4.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallTypeChange_LinkUrlShouldBeUpdatable(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Create with type 1 (no link) + var createRequest = new FloatBallCreateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = 1, + Image = "http://test.com/img.jpg", + LinkUrl = null, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + var id = service.CreateFloatBallAsync(createRequest).GetAwaiter().GetResult(); + + // Update to type 2 with link + var newLinkUrl = $"/page/updated{seed.Get}"; + var updateRequest = new FloatBallUpdateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = 2, // Change to 跳转页面 + Image = "http://test.com/img.jpg", + LinkUrl = newLinkUrl, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + var result = service.UpdateFloatBallAsync(id, updateRequest).GetAwaiter().GetResult(); + if (!result) return false; + + var updated = dbContext.FloatBallConfigs.Find(id); + return updated != null && + updated.Type == 2 && + updated.LinkUrl == newLinkUrl; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** + /// When FloatBall type changes from 2 to 1, the LinkUrl should be preserved but not used. + /// **Validates: Requirements 4.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallTypeChange_FromType2ToType1_ShouldPreserveLinkUrl(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var originalLinkUrl = $"/page/original{seed.Get}"; + + // Create with type 2 (with link) + var createRequest = new FloatBallCreateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = 2, + Image = "http://test.com/img.jpg", + LinkUrl = originalLinkUrl, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + var id = service.CreateFloatBallAsync(createRequest).GetAwaiter().GetResult(); + + // Update to type 1 (展示图片) + var updateRequest = new FloatBallUpdateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = 1, // Change to 展示图片 + Image = "http://test.com/img.jpg", + LinkUrl = originalLinkUrl, // Keep the link URL + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + var result = service.UpdateFloatBallAsync(id, updateRequest).GetAwaiter().GetResult(); + if (!result) return false; + + var updated = dbContext.FloatBallConfigs.Find(id); + // Type should be 1, and LinkUrl should be preserved (even if not used) + return updated != null && + updated.Type == 1 && + updated.LinkUrl == originalLinkUrl; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** + /// For any FloatBall, the type field should correctly determine the behavior. + /// Type 1 = 展示图片 (show image), Type 2 = 跳转页面 (jump to page). + /// **Validates: Requirements 4.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallType_ShouldDetermineBehavior(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var validTypes = new[] { 1, 2 }; + var selectedType = validTypes[seed.Get % validTypes.Length]; + var linkUrl = selectedType == 2 ? $"/page/test{seed.Get}" : null; + + var request = new FloatBallCreateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = selectedType, + Image = "http://test.com/img.jpg", + LinkUrl = linkUrl, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + var created = dbContext.FloatBallConfigs.Find(id); + + // Verify type is correctly stored + return created != null && created.Type == selectedType; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** + /// The response should correctly reflect the type and LinkUrl relationship. + /// **Validates: Requirements 4.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallResponse_ShouldReflectTypeAndLinkUrl(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var validTypes = new[] { 1, 2 }; + var selectedType = validTypes[seed.Get % validTypes.Length]; + var linkUrl = selectedType == 2 ? $"/page/test{seed.Get}" : string.Empty; + + var request = new FloatBallCreateRequest + { + Title = $"Test FloatBall {seed.Get}", + Type = selectedType, + Image = "http://test.com/img.jpg", + LinkUrl = linkUrl, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1 + }; + + var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); + var response = service.GetFloatBallByIdAsync(id).GetAwaiter().GetResult(); + + // Verify response correctly reflects type and LinkUrl + return response != null && + response.Type == selectedType && + response.LinkUrl == (linkUrl ?? string.Empty); + } + + #endregion + + #region Helper Methods + + private HoneyBoxDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + return new HoneyBoxDbContext(options); + } + + #endregion +} + + +/// +/// 内容与辅助模块前端属性测试 - 第四部分 +/// Feature: content-auxiliary-frontend +/// +public class ContentAuxiliaryFrontendPropertyTests_Part4 +{ + private readonly Mock> _mockFloatBallLogger = new(); + private readonly Mock> _mockWelfareHouseLogger = new(); + + #region Property 4: API响应格式一致性 + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// For any FloatBall list API response, the response format should conform to the unified + /// PagedResult structure with correct pagination parameters. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallApiResponse_ShouldHaveConsistentPagedStructure(PositiveInt seed) + { + var itemCount = (seed.Get % 20) + 5; + var page = (seed.Get % 3) + 1; + var pageSize = (seed.Get % 10) + 5; + + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Create test float balls + for (int i = 0; i < itemCount; i++) + { + dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); + } + dbContext.SaveChanges(); + + var request = new FloatBallListRequest { Page = page, PageSize = pageSize }; + var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); + + // Verify PagedResult structure + return result != null && + result.List != null && + result.Total >= 0 && + result.Page == page && + result.PageSize == pageSize && + result.TotalPages == (int)Math.Ceiling((double)result.Total / result.PageSize); + } + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// For any WelfareHouse list API response, the response format should conform to the unified + /// PagedResult structure with correct pagination parameters. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool WelfareHouseApiResponse_ShouldHaveConsistentPagedStructure(PositiveInt seed) + { + var itemCount = (seed.Get % 20) + 5; + var page = (seed.Get % 3) + 1; + var pageSize = (seed.Get % 10) + 5; + + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + // Create test welfare house entries + for (int i = 0; i < itemCount; i++) + { + dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); + } + dbContext.SaveChanges(); + + var request = new WelfareHouseListRequest { Page = page, PageSize = pageSize }; + var result = service.GetWelfareHousesAsync(request).GetAwaiter().GetResult(); + + // Verify PagedResult structure + return result != null && + result.List != null && + result.Total >= 0 && + result.Page == page && + result.PageSize == pageSize && + result.TotalPages == (int)Math.Ceiling((double)result.Total / result.PageSize); + } + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// For any FloatBall detail API response, all required fields should be present. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallDetailResponse_ShouldHaveAllRequiredFields(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + var floatBall = CreateTestFloatBall($"FloatBall{seed.Get}"); + dbContext.FloatBallConfigs.Add(floatBall); + dbContext.SaveChanges(); + + var response = service.GetFloatBallByIdAsync(floatBall.Id).GetAwaiter().GetResult(); + + // Verify all required fields are present + return response != null && + response.Id > 0 && + !string.IsNullOrEmpty(response.Image) && + !string.IsNullOrEmpty(response.PositionX) && + !string.IsNullOrEmpty(response.PositionY) && + !string.IsNullOrEmpty(response.Width) && + !string.IsNullOrEmpty(response.Height) && + response.Type >= 1 && response.Type <= 2 && + response.Effect >= 0 && response.Effect <= 1 && + response.Status >= 0 && response.Status <= 1; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// For any WelfareHouse detail API response, all required fields should be present. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool WelfareHouseDetailResponse_ShouldHaveAllRequiredFields(PositiveInt seed) + { + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + var welfareHouse = CreateTestWelfareHouse($"WelfareHouse{seed.Get}", seed.Get % 100); + dbContext.WelfareHouses.Add(welfareHouse); + dbContext.SaveChanges(); + + var response = service.GetWelfareHouseByIdAsync(welfareHouse.Id).GetAwaiter().GetResult(); + + // Verify all required fields are present + return response != null && + response.Id > 0 && + !string.IsNullOrEmpty(response.Name) && + !string.IsNullOrEmpty(response.Image) && + !string.IsNullOrEmpty(response.Url) && + response.Sort >= 0 && + response.Status >= 0 && response.Status <= 1; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// For any FloatBall list item, all required fields should be present. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool FloatBallListItem_ShouldHaveAllRequiredFields(PositiveInt seed) + { + var itemCount = (seed.Get % 10) + 1; + + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Create test float balls + for (int i = 0; i < itemCount; i++) + { + dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); + } + dbContext.SaveChanges(); + + var request = new FloatBallListRequest { Page = 1, PageSize = 100 }; + var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); + + // Verify all items have required fields + return result.List.All(item => + item.Id > 0 && + !string.IsNullOrEmpty(item.Image) && + !string.IsNullOrEmpty(item.PositionX) && + !string.IsNullOrEmpty(item.PositionY) && + !string.IsNullOrEmpty(item.Width) && + !string.IsNullOrEmpty(item.Height) && + item.Type >= 1 && item.Type <= 2 && + item.Effect >= 0 && item.Effect <= 1 && + item.Status >= 0 && item.Status <= 1); + } + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// For any WelfareHouse list item, all required fields should be present. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool WelfareHouseListItem_ShouldHaveAllRequiredFields(PositiveInt seed) + { + var itemCount = (seed.Get % 10) + 1; + + using var dbContext = CreateDbContext(); + var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + // Create test welfare house entries + for (int i = 0; i < itemCount; i++) + { + dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); + } + dbContext.SaveChanges(); + + var request = new WelfareHouseListRequest { Page = 1, PageSize = 100 }; + var result = service.GetWelfareHousesAsync(request).GetAwaiter().GetResult(); + + // Verify all items have required fields + return result.List.All(item => + item.Id > 0 && + !string.IsNullOrEmpty(item.Name) && + !string.IsNullOrEmpty(item.Image) && + !string.IsNullOrEmpty(item.Url) && + item.Sort >= 0 && + item.Status >= 0 && item.Status <= 1); + } + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// PagedResult should correctly calculate HasNextPage and HasPreviousPage. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool PagedResult_ShouldCorrectlyCalculateNavigationFlags(PositiveInt seed) + { + var itemCount = (seed.Get % 30) + 15; // 15 to 44 items + var pageSize = 5; + var totalPages = (int)Math.Ceiling((double)itemCount / pageSize); + var page = (seed.Get % totalPages) + 1; // Valid page number + + using var dbContext = CreateDbContext(); + var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + + // Create test float balls + for (int i = 0; i < itemCount; i++) + { + dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); + } + dbContext.SaveChanges(); + + var request = new FloatBallListRequest { Page = page, PageSize = pageSize }; + var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); + + // Verify navigation flags + var expectedHasNextPage = page < result.TotalPages; + var expectedHasPreviousPage = page > 1; + + return result.HasNextPage == expectedHasNextPage && + result.HasPreviousPage == expectedHasPreviousPage; + } + + /// + /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** + /// Empty result should return valid PagedResult with empty list. + /// **Validates: Requirements 11.4, 11.5** + /// + [Property(MaxTest = 100)] + public bool EmptyResult_ShouldReturnValidPagedResult(PositiveInt seed) + { + var page = (seed.Get % 5) + 1; + var pageSize = (seed.Get % 10) + 5; + + using var dbContext = CreateDbContext(); + var floatBallService = new FloatBallService(dbContext, _mockFloatBallLogger.Object); + var welfareHouseService = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); + + // Don't add any data - test empty result + + var floatBallResult = floatBallService.GetFloatBallsAsync(new FloatBallListRequest { Page = page, PageSize = pageSize }).GetAwaiter().GetResult(); + var welfareHouseResult = welfareHouseService.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = page, PageSize = pageSize }).GetAwaiter().GetResult(); + + // Verify empty results have valid structure + return floatBallResult != null && + floatBallResult.List != null && + floatBallResult.List.Count == 0 && + floatBallResult.Total == 0 && + floatBallResult.Page == page && + floatBallResult.PageSize == pageSize && + welfareHouseResult != null && + welfareHouseResult.List != null && + welfareHouseResult.List.Count == 0 && + welfareHouseResult.Total == 0 && + welfareHouseResult.Page == page && + welfareHouseResult.PageSize == pageSize; + } + + #endregion + + #region Helper Methods + + private HoneyBoxDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + return new HoneyBoxDbContext(options); + } + + private FloatBallConfig CreateTestFloatBall(string title) + { + return new FloatBallConfig + { + Title = title, + Type = 1, + Image = "http://test.com/floatball.jpg", + LinkUrl = string.Empty, + PositionX = "10", + PositionY = "20", + Width = "50", + Height = "50", + Effect = 0, + Status = 1, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now + }; + } + + private WelfareHouse CreateTestWelfareHouse(string name, int sort) + { + return new WelfareHouse + { + Name = name, + Image = "http://test.com/welfare.jpg", + Url = "/welfare/test", + Sort = sort, + Status = 1, + CreateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), + UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds() + }; + } + + #endregion +}