From 43f8a88394bcb1d0e6cdddaa95f707aaaae72f0c Mon Sep 17 00:00:00 2001 From: zpc Date: Sat, 3 Jan 2026 21:06:26 +0800 Subject: [PATCH] 21 --- .kiro/specs/float-ball-migration/design.md | 290 ++++++++++++++++++ .../float-ball-migration/requirements.md | 73 +++++ .kiro/specs/float-ball-migration/tasks.md | 84 +++++ .../.vs/HoneyBox/DesignTimeBuild/.dtbcache.v2 | Bin 343500 -> 343647 bytes server/C#/HoneyBox/.vs/HoneyBox/v18/.suo | Bin 77312 -> 81408 bytes .../HoneyBox/v18/DocumentLayout.backup.json | 69 +++-- .../.vs/HoneyBox/v18/DocumentLayout.json | 123 +++++--- .../Controllers/ConfigController.cs | 39 ++- .../Controllers/WarehouseController.cs | 2 +- .../Interfaces/IFloatBallService.cs | 15 + .../Services/FloatBallService.cs | 61 ++++ .../Modules/ServiceModule.cs | 8 + .../HoneyBox.Model/Data/HoneyBoxDbContext.cs | 93 ++++++ .../Entities/FloatBallConfig.cs | 104 +++++++ .../Models/FloatBall/FloatBallResponse.cs | 106 +++++++ server/scripts/migrate_float_ball.js | 267 ++++++++++++++++ 16 files changed, 1268 insertions(+), 66 deletions(-) create mode 100644 .kiro/specs/float-ball-migration/design.md create mode 100644 .kiro/specs/float-ball-migration/requirements.md create mode 100644 .kiro/specs/float-ball-migration/tasks.md create mode 100644 server/C#/HoneyBox/src/HoneyBox.Core/Interfaces/IFloatBallService.cs create mode 100644 server/C#/HoneyBox/src/HoneyBox.Core/Services/FloatBallService.cs create mode 100644 server/C#/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs create mode 100644 server/C#/HoneyBox/src/HoneyBox.Model/Models/FloatBall/FloatBallResponse.cs create mode 100644 server/scripts/migrate_float_ball.js diff --git a/.kiro/specs/float-ball-migration/design.md b/.kiro/specs/float-ball-migration/design.md new file mode 100644 index 00000000..fe05709c --- /dev/null +++ b/.kiro/specs/float-ball-migration/design.md @@ -0,0 +1,290 @@ +# Design Document: Float Ball Migration + +## Overview + +本设计文档描述悬浮球功能从 PHP 后端迁移到 .NET 10 后端的技术方案。包括数据库表设计、数据迁移脚本、API 接口实现和 Entity Framework 配置。 + +悬浮球是首页显示的可点击浮动图标,支持两种交互方式: +1. **展示图片** (type=1): 点击后弹出图片弹窗 +2. **跳转页面** (type=2): 点击后跳转到指定页面 + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend (UniApp) │ +│ FloatBall.vue Component │ +└─────────────────────────────────────────────────────────────┘ + │ + │ GET /api/getFloatBall + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ HoneyBox.Api Layer │ +│ ConfigController.cs │ +│ [AllowAnonymous] endpoint │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ HoneyBox.Core Layer │ +│ IFloatBallService.cs │ +│ FloatBallService.cs │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ HoneyBox.Model Layer │ +│ FloatBallConfig.cs (Entity) │ +│ FloatBallResponse.cs (DTO) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ SQL Server │ +│ float_ball_configs table │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Components and Interfaces + +### 1. Database Table: float_ball_configs + +```sql +CREATE TABLE float_ball_configs ( + id INT IDENTITY(1,1) PRIMARY KEY, + status TINYINT NOT NULL DEFAULT 0, -- 状态: 0关闭 1开启 + type TINYINT NOT NULL DEFAULT 1, -- 类型: 1展示图片 2跳转页面 + image NVARCHAR(255) NOT NULL DEFAULT '', -- 悬浮球图片URL + link_url NVARCHAR(255) NOT NULL DEFAULT '', -- 跳转链接 + position_x NVARCHAR(30) NOT NULL DEFAULT '', -- X轴位置 + position_y NVARCHAR(30) NOT NULL DEFAULT '', -- Y轴位置 + width NVARCHAR(30) NOT NULL DEFAULT '', -- 宽度 + height NVARCHAR(30) NOT NULL DEFAULT '', -- 高度 + effect TINYINT NOT NULL DEFAULT 0, -- 特效: 0无 1缩放动画 + title NVARCHAR(255) NULL, -- 标题 + image_details NVARCHAR(255) NULL, -- 详情图片URL + image_bj NVARCHAR(255) NULL, -- 背景图片URL + image_details_x NVARCHAR(255) NULL, -- 详情图片X偏移 + image_details_y NVARCHAR(255) NULL, -- 详情图片Y偏移 + image_details_w NVARCHAR(255) NULL, -- 详情图片宽度 + image_details_h NVARCHAR(255) NULL, -- 详情图片高度 + created_at DATETIME2 NOT NULL DEFAULT GETDATE(), + updated_at DATETIME2 NOT NULL DEFAULT GETDATE() +); +``` + +### 2. Entity Class: FloatBallConfig + +```csharp +namespace HoneyBox.Model.Entities; + +public class FloatBallConfig +{ + public int Id { get; set; } + public byte Status { get; set; } + public byte Type { get; set; } + public string Image { get; set; } = string.Empty; + public string LinkUrl { get; set; } = string.Empty; + public string PositionX { get; set; } = string.Empty; + public string PositionY { get; set; } = string.Empty; + public string Width { get; set; } = string.Empty; + public string Height { get; set; } = string.Empty; + public byte Effect { get; set; } + 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; } +} +``` + +### 3. Response DTO: FloatBallResponse + +```csharp +namespace HoneyBox.Model.Models.FloatBall; + +public class FloatBallResponse +{ + public int Id { get; set; } + public int Type { get; set; } + public string Image { get; set; } = string.Empty; + public string LinkUrl { get; set; } = string.Empty; + public string PositionX { get; set; } = string.Empty; + public string PositionY { get; set; } = string.Empty; + public string Width { get; set; } = string.Empty; + public string Height { get; set; } = string.Empty; + public int Effect { get; set; } + 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; } +} +``` + +### 4. Service Interface: IFloatBallService + +```csharp +namespace HoneyBox.Core.Interfaces; + +public interface IFloatBallService +{ + Task> GetEnabledFloatBallsAsync(); +} +``` + +### 5. API Endpoint + +``` +GET /api/getFloatBall +Authorization: None (AllowAnonymous) +Response: ApiResponse> +``` + +## Data Models + +### MySQL Source Table Schema (float_ball_config) + +| Column | Type | Description | +|--------|------|-------------| +| id | int(11) | 主键ID | +| status | tinyint(1) | 状态: 0关闭 1开启 | +| type | tinyint(1) | 类型: 1展示图片 2跳转页面 | +| image | varchar(255) | 悬浮球图片URL | +| link_url | varchar(255) | 跳转链接 | +| position_x | varchar(30) | X轴位置 (如: -0%, 10px) | +| position_y | varchar(30) | Y轴位置 (如: 70vh, 21vh) | +| width | varchar(30) | 宽度 (如: 150rpx, 52rpx) | +| height | varchar(30) | 高度 (如: 165rpx, 120rpx) | +| effect | tinyint(1) | 特效: 0无 1缩放动画 | +| create_time | int(11) | 创建时间 (Unix时间戳) | +| update_time | int(11) | 更新时间 (Unix时间戳) | +| title | varchar(255) | 标题 | +| image_details | varchar(255) | 详情图片URL | +| image_bj | varchar(255) | 背景图片URL | +| image_details_x | varchar(255) | 详情图片X偏移 | +| image_details_y | varchar(255) | 详情图片Y偏移 | +| image_details_w | varchar(255) | 详情图片宽度 | +| image_details_h | varchar(255) | 详情图片高度 | + +### Data Migration Mapping + +| MySQL Column | SQL Server Column | Transformation | +|--------------|-------------------|----------------| +| id | id | 保持原值 (IDENTITY_INSERT ON) | +| status | status | 直接映射 | +| type | type | 直接映射 | +| image | image | 直接映射 | +| link_url | link_url | 直接映射 | +| position_x | position_x | 直接映射 | +| position_y | position_y | 直接映射 | +| width | width | 直接映射 | +| height | height | 直接映射 | +| effect | effect | 直接映射 | +| title | title | 直接映射 | +| image_details | image_details | 直接映射 | +| image_bj | image_bj | 直接映射 | +| image_details_x | image_details_x | 直接映射 | +| image_details_y | image_details_y | 直接映射 | +| image_details_w | image_details_w | 直接映射 | +| image_details_h | image_details_h | 直接映射 | +| create_time | created_at | Unix时间戳 → DATETIME2 | +| update_time | updated_at | Unix时间戳 → DATETIME2 | + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Data Migration Record Count Consistency + +*For any* migration execution, the number of records in SQL Server `float_ball_configs` table after migration SHALL equal the number of records in MySQL `float_ball_config` table. + +**Validates: Requirements 2.4** + +### Property 2: Data Migration ID Preservation + +*For any* record migrated from MySQL to SQL Server, the `id` value in the target table SHALL equal the `id` value in the source table. + +**Validates: Requirements 2.3** + +### Property 3: Timestamp Transformation Validity + +*For any* Unix timestamp value from MySQL, the transformed DATETIME2 value in SQL Server SHALL represent the same point in time. + +**Validates: Requirements 2.2** + +### Property 4: API Returns Only Enabled Configurations + +*For any* GET request to `/api/getFloatBall`, all returned configurations SHALL have status equal to 1 (enabled) in the database. + +**Validates: Requirements 3.1, 3.2** + +### Property 5: API Response Field Completeness + +*For any* configuration returned by the API, the response SHALL contain all required fields (id, type, image, link_url, position_x, position_y, width, height, effect, title, image_details, image_bj, image_details_x, image_details_y, image_details_w, image_details_h) and SHALL NOT contain status, created_at, updated_at fields. + +**Validates: Requirements 3.3, 3.4** + +### Property 6: API Response Format Consistency + +*For any* successful API response, the format SHALL be `{ "status": 1, "msg": "...", "data": [...] }` with status equal to 1. + +**Validates: Requirements 5.1, 5.2** + +### Property 7: Image URL Preservation + +*For any* configuration with image URLs (image, image_details, image_bj), the API response SHALL return the URLs unchanged from the database values. + +**Validates: Requirements 5.4** + +### Property 8: Incremental Migration Idempotence + +*For any* migration script execution, running the migration twice SHALL result in the same final state (no duplicate records). + +**Validates: Requirements 2.5** + +## Error Handling + +### Migration Script Errors + +1. **Connection Failure**: Log error and exit with non-zero code +2. **Single Record Insert Failure**: Log error, continue with remaining records +3. **Batch Insert Failure**: Fall back to single record inserts + +### API Errors + +1. **Database Connection Error**: Return `{ "status": 0, "msg": "获取悬浮球配置失败", "data": null }` +2. **Unexpected Exception**: Log error, return generic error message + +## Testing Strategy + +### Unit Tests + +- Test FloatBallService.GetEnabledFloatBallsAsync() returns only enabled configs +- Test response DTO mapping excludes status, created_at, updated_at +- Test empty result when no enabled configs exist + +### Property-Based Tests + +使用 xUnit + FsCheck 进行属性测试: + +1. **Property 1**: 验证迁移后记录数一致性 +2. **Property 4**: 验证 API 只返回启用的配置 +3. **Property 8**: 验证迁移脚本幂等性 + +### Integration Tests + +- Test full API endpoint `/api/getFloatBall` returns correct format +- Test migration script with test database + +### Test Configuration + +- Property tests: 最少 100 次迭代 +- 使用 FsCheck 生成随机测试数据 +- 标签格式: **Feature: float-ball-migration, Property {number}: {property_text}** diff --git a/.kiro/specs/float-ball-migration/requirements.md b/.kiro/specs/float-ball-migration/requirements.md new file mode 100644 index 00000000..c689fb12 --- /dev/null +++ b/.kiro/specs/float-ball-migration/requirements.md @@ -0,0 +1,73 @@ +# Requirements Document + +## Introduction + +悬浮球功能迁移 - 将悬浮球配置管理功能从 PHP (ThinkPHP) 后端迁移到 .NET 10 后端。包括数据库表迁移、数据迁移和 API 接口迁移。悬浮球是首页显示的可点击浮动图标,支持展示图片弹窗或跳转页面两种交互方式。 + +## Glossary + +- **Float_Ball_System**: 悬浮球系统,负责管理和展示首页悬浮球配置 +- **Float_Ball_Config**: 悬浮球配置实体,存储单个悬浮球的所有配置信息 +- **Float_Ball_Service**: 悬浮球业务服务,处理悬浮球相关的业务逻辑 +- **Migration_Script**: 数据迁移脚本,负责将 MySQL 数据迁移到 SQL Server + +## Requirements + +### Requirement 1: 数据库表迁移 + +**User Story:** As a developer, I want to create the float_ball_configs table in SQL Server, so that the system can store float ball configuration data. + +#### Acceptance Criteria + +1. THE Float_Ball_System SHALL create a `float_ball_configs` table in SQL Server with all required columns +2. THE Float_Ball_System SHALL support the following columns: id, status, type, image, link_url, position_x, position_y, width, height, effect, title, image_details, image_bj, image_details_x, image_details_y, image_details_w, image_details_h, created_at, updated_at +3. THE Float_Ball_System SHALL use appropriate SQL Server data types matching the original MySQL schema +4. THE Float_Ball_System SHALL set id as primary key with auto-increment + +### Requirement 2: 数据迁移 + +**User Story:** As a developer, I want to migrate existing float ball data from MySQL to SQL Server, so that the new system has all historical configurations. + +#### Acceptance Criteria + +1. THE Migration_Script SHALL read all records from MySQL `float_ball_config` table +2. THE Migration_Script SHALL transform Unix timestamps to SQL Server DATETIME2 format +3. THE Migration_Script SHALL insert all records into SQL Server `float_ball_configs` table preserving original IDs +4. THE Migration_Script SHALL verify record count consistency after migration +5. THE Migration_Script SHALL support incremental migration (skip already migrated records) +6. IF migration fails for a record, THEN THE Migration_Script SHALL log the error and continue with remaining records + +### Requirement 3: 获取悬浮球配置接口 + +**User Story:** As a frontend developer, I want to call the getFloatBall API, so that I can display float balls on the homepage. + +#### Acceptance Criteria + +1. WHEN a GET request is made to `/api/getFloatBall`, THE Float_Ball_System SHALL return all enabled float ball configurations +2. THE Float_Ball_System SHALL only return configurations where status equals 1 (enabled) +3. THE Float_Ball_System SHALL return the following fields for each configuration: id, type, image, link_url, position_x, position_y, width, height, effect, title, image_details, image_bj, image_details_x, image_details_y, image_details_w, image_details_h +4. THE Float_Ball_System SHALL NOT return status, created_at, updated_at fields in the response +5. THE Float_Ball_System SHALL return an empty array if no enabled configurations exist +6. THE Float_Ball_System SHALL NOT require authentication for this endpoint + +### Requirement 4: Entity Framework 实体配置 + +**User Story:** As a developer, I want to create the FloatBallConfig entity class, so that Entity Framework can map the database table correctly. + +#### Acceptance Criteria + +1. THE Float_Ball_System SHALL create a `FloatBallConfig` entity class in HoneyBox.Model +2. THE Float_Ball_System SHALL configure proper column mappings using Fluent API or Data Annotations +3. THE Float_Ball_System SHALL register the entity in HoneyBoxDbContext +4. THE Float_Ball_System SHALL support nullable fields for optional columns (title, image_details, image_bj, etc.) + +### Requirement 5: 响应格式兼容性 + +**User Story:** As a frontend developer, I want the API response format to be compatible with the existing frontend, so that no frontend changes are required. + +#### Acceptance Criteria + +1. THE Float_Ball_System SHALL return response in format: `{ "status": 1, "msg": "获取悬浮球配置成功", "data": [...] }` +2. THE Float_Ball_System SHALL return status 1 for successful requests +3. THE Float_Ball_System SHALL return status 0 for failed requests with error message +4. WHEN returning image URLs, THE Float_Ball_System SHALL preserve the original URL format (already contains full CDN path) diff --git a/.kiro/specs/float-ball-migration/tasks.md b/.kiro/specs/float-ball-migration/tasks.md new file mode 100644 index 00000000..fd2729e0 --- /dev/null +++ b/.kiro/specs/float-ball-migration/tasks.md @@ -0,0 +1,84 @@ +# Implementation Plan: Float Ball Migration + +## Overview + +将悬浮球功能从 PHP 后端迁移到 .NET 10 后端,包括数据库表创建、数据迁移脚本和 API 接口实现。 + +## Tasks + +- [x] 1. 创建数据库表和 Entity 配置 + - [x] 1.1 创建 SQL Server 表 `float_ball_configs` + - 在 SQL Server 中执行建表语句 + - 包含所有必要字段和约束 + - _Requirements: 1.1, 1.2, 1.3, 1.4_ + + - [x] 1.2 创建 FloatBallConfig 实体类 + - 在 `HoneyBox.Model/Entities/` 目录创建 `FloatBallConfig.cs` + - 定义所有属性和数据类型 + - _Requirements: 4.1, 4.4_ + + - [x] 1.3 配置 DbContext + - 在 `HoneyBoxDbContext.cs` 中添加 `DbSet` + - 配置 Fluent API 映射 + - _Requirements: 4.2, 4.3_ + +- [x] 2. 创建数据迁移脚本 + - [x] 2.1 创建 `migrate_float_ball.js` 迁移脚本 + - 参考 `migrate_coupons.js` 的模式 + - 实现 MySQL 到 SQL Server 的数据迁移 + - 支持增量迁移(跳过已迁移记录) + - 包含错误处理和日志记录 + - _Requirements: 2.1, 2.2, 2.3, 2.5, 2.6_ + + - [x] 2.2 执行数据迁移并验证 + - 运行迁移脚本 + - 验证记录数一致性 + - _Requirements: 2.4_ + +- [x] 3. 实现 API 接口 + - [x] 3.1 创建 FloatBallResponse DTO + - 在 `HoneyBox.Model/Models/FloatBall/` 目录创建响应模型 + - 排除 status, created_at, updated_at 字段 + - _Requirements: 3.3, 3.4_ + + - [x] 3.2 创建 IFloatBallService 接口和实现 + - 在 `HoneyBox.Core/Interfaces/` 创建接口 + - 在 `HoneyBox.Core/Services/` 创建实现 + - 实现 GetEnabledFloatBallsAsync 方法 + - _Requirements: 3.1, 3.2_ + + - [x] 3.3 创建 API 端点 + - 在 `ConfigController.cs` 或新建 `FloatBallController.cs` 添加端点 + - 实现 `GET /api/getFloatBall` 接口 + - 设置 `[AllowAnonymous]` 属性 + - _Requirements: 3.5, 3.6, 5.1, 5.2, 5.3_ + + - [x] 3.4 注册服务依赖 + - 在 Autofac 模块中注册 IFloatBallService + - _Requirements: 4.3_ + +- [x] 4. Checkpoint - 验证功能 + - 确保所有代码编译通过 + - 测试 API 接口返回正确数据 + - 验证响应格式与 PHP 后端一致 + - 如有问题请询问用户 + +- [ ]* 5. 编写测试 + - [ ]* 5.1 编写单元测试 + - 测试 FloatBallService 只返回启用的配置 + - 测试空结果场景 + - **Property 4: API Returns Only Enabled Configurations** + - **Validates: Requirements 3.1, 3.2** + + - [ ]* 5.2 编写集成测试 + - 测试完整 API 端点 + - 验证响应格式 + - **Property 6: API Response Format Consistency** + - **Validates: Requirements 5.1, 5.2** + +## Notes + +- 任务标记 `*` 为可选任务,可跳过以加快 MVP 开发 +- 每个任务引用具体需求以便追溯 +- 数据迁移脚本参考现有的 `migrate_coupons.js` 模式 +- API 端点无需认证,使用 `[AllowAnonymous]` 属性 diff --git a/server/C#/HoneyBox/.vs/HoneyBox/DesignTimeBuild/.dtbcache.v2 b/server/C#/HoneyBox/.vs/HoneyBox/DesignTimeBuild/.dtbcache.v2 index 804a336107134c79ae5e22671008d0b64e3d6312..49fdc6b80327083e72b6dc1207865517ef6617fd 100644 GIT binary patch delta 35711 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 delta 34653 zcmeI52Xqw2^6rtQcV{&tL=rhilTknj5tnQ-*nlxv2+0Wy*aio}3S%6|d9lfkeo{{8`ZNjbKz#}p&PLg zV`oKl3C0gj9Fm@q+AujKC9Qs9T5@_~V)~H8l(giu_|&wF}q>B&DauB_<~| zh)W!tk%}oPL*kM%1`o-YHKSBygQShC8|>X2xi3X*zW<|>2V2J_rzE5{XqcKlIHO_x zxTMth)b#X(wA6;_8EJ`WacQaP^;7Dn){jd_s-F>`kQCQ2B|g4ELTYk)dRkn3N?b;A zJ|jLUYs9>Wt}kbuts7a+pH(p_vh&0?L+U4_#HA+0B@P*!*f1k4IX=AOnX@j(=hE*2t{QTa7gfVp5{> zOgD-F&>1PicPL$Ft#*o zL$IY|%fR*kwg<62giVh1Ft$gq4aGJLTPe3tdB>Y#Ylf{kwieMv^5uIJgW=fZid$hD z5q_$>s292y!<py9Zegu^eVO!g7@57|QWTIKjP>ET>R#>`%D&Da&Up zpR;_ya+>7~%UO~Toa63!6ddLP%SARXv3$w$70cHw->`hk@*N76_C5E0V7bh4h2=+< zt0=glYupQ6N5$>sa2Hs9LcyGD?)}X23(K!42>iyq-%&905AOZRa)ae2%Pp4MEO$^4 z$Y;8VcUQy9|E) zRcE&byEVC2i={S89bQ(Kd-Yi2Q1Bkcb1#8=i7fTmOk!!kW-?1dHd9dK>#q^JjZv_u zCcLmIOEZ?{EJ2hO2DIc}E0)$QZCKi}v_rwc?lxve%!Ky%cNTOo+GRsW13FQ!vvD_m zQg^}8IvdcH{&q8zzM`)ufAWM@NP!->g; ze&NfrMX6|V`WsFG=@gtJiiR^!2p`)645zTPXPr=q0}ZE$bf!F@oI!>YE1iwnNj03J z(z$+7l*&)T4>lkT%Ni2C{eURRL#G=~ahWtezt629!*EJSC-ssV`T@f!DV^aDD(6AN zDJ308Qx6$VY3Wp$t8yMToHEk6q^U;?r>t~_>YSm5Q%*W{9ulPzsKjB0Qyv}MJb1$} zhh#ey)RueHa4JadydG(|;Z&5)QXGlz*$Bg_B%PY`lrz$BDodxEb{;dFD$*(Ru*!Md zaH>jYkgX`46IKvU8UzVU*$Ag|=JhU0f(jwo_eYjW(R>5{{X#R`8_Z)R4|m zy~?Kyr>1oJ=$xkwrw!5V+xrD9O*1XAPJrt@%Th^PB;bDCwq7dftE+$m*oCUNqn( zvVzwXIN5+HWDP9hb4Q%|?nr*-wvf_rR;pZAKkF2t> zYWVpEgvlDKvlbZeGFc<8EASNq7LqkXgNqDUOx86GE-_#!S!ZO0t$Y3S4EtYO;1}aE$?L$tpKoWvw${Jy~`! zRm27ZHj)*utxX1OCTpe!-!@trpI|gKtbr!SuiL=#!ccqop$ci4R zO8wA)kEE5W)E;iBJyoe68*q@o@+DLehYUDO)_5F?pY}%#I7-$dI_sDL$H|(dtrG^E zB+EgN-)v49P9OPdJX^x&zSn+Yz^9Zn;Ad6oX9j#Otz4D%b1UtyD*eKM(*$-csVY5V zz*(}6;aFVhIRnm{n@|RM>e`CP6WKI7?mHM3l z-%Bf3sYBdS(^aWI7;u@ukRI!b0Y6G>{;z6Aat+Hy3F#-yZ)m>W!1r#D{!)OH-7Epwii zKvA;F{h>-NCZM>qa+NyPEp?nKwS<6@1QsZ#iYO(ZG+8roEPkez5m1(_zB;R%fbwKb z&{hQj70J5zqyj4ms7%&jomE9ZRkFJLsYH=yI*u1X0=>XVn!@kF1T_iW3k|R>7wgm>?jLtXn#(zJMgM#@|q-HV}|3tz4x} zbxWP5N^K}0g}~tzRH=;wG$zZzvAEPG0-BQbzRqeUpgCEWv=tQ4f~?>(3T!E$6Vu4gxxoRpFLeV zR-sC&h;9P9lQm+D0`C!UFIoL{Ru2I^$vTh2^UJE2fcvDCtJFnqsf$&qy#?G);88tR z9|3*I>ULYLv7dncWRKY(mAXzQ6HAp}zS*Ap+9Lin*go%@FW_ zv~rdDx?AcSs?-MsJVf9fJ=Vhl9w93e$KofzPyxfp>R3gU`lx{6WR29;2mvF>IyO#$ zj|q63tnE7M2?3*|HLsca;y7BslhVmm=^D4vwW`vm1UyYc)vK!BFP{<4I()yJw_d$n zju9}Ha>if|mp)Fwc(RsjDDvW{zThJcx5#bv6xW(k;0)-nyw5ipmmu{vv> zfca$osI$TX7LYaLF15y&1-wGmH4QElu!yWPI%~0jC1gcZSKv|s%g7ouLDjWfz^i0+ z)>*F!c%7_rLOrf;2zXOkxgOWO?&G>oJ+5yFSV7=UJ=RJAtH_#(WAVedTEH5z>>A2i zD_|X2@!DE1U;~5Cs!}%!*hJPiowZrO+hld|sZzHHct={fN;fodR|-I8l|lTfiQ&*6OSe1nebiykC{NPr!a@ z`3y5=!RMk=HvWMxaAl|E*k@?4v-J0zn^Lwj zoELC`yo>br68>EbUy3g9l?Zp=D0-C3%zWD2pHZ}SzI;x;iB5!b(ZO$0!tuea@bXEb zQo-AzWj5T#HQkZH9T9HUL%bT^Gf5QA8uh$*CZdGXr)9SMx?J0b%WM0xp^gmd_`>Fk z;*}E42waNaxtdF}IW7&#An40__eEhxmT=bLVtk7kBi%8$c&rS@`m)YW7F{Aj&S$eD z=D@4IRyccbjvs++c+Ks?>ppnHCtY|mSB7(5Qg8WM%VjR-T+GICvSEb}R(ck^h6`5t zVD;Y@bi#qNVU5rE7I)ONiEzFPI^PGKAA-*1AguKTogahF)u3}N22pe(9CSSN|AJNyp8RNHo;X%{HDk~TfgbUce=nKbA7j3Ox*#8Op z*}m|Q>Eg0A7yEVnL8#{s_nsl<_u7H|o!E9^+l{R!F6)JDFUI?@?Z4#I8kCvX$E2U=icKw?(dc_9c7V5p=6F zp-`VjaYFDSp+hx#$q!$8P`^S=jeg~auL<3z(Qo|ltp^P#G)kl2`Qdv)qZg=xfAGU) z4{9mY*60;K{77g&jb8P`H4hrC(EJ*`?gxj^H5$c9M)0693N4_~pZt(b=uM6O?1x`G zXhDS*(&(>#_>Is`FRT0dyC44WpoJA$M58!I`6r=^G)O z#1eW)qeV?9=0Phdw6aEV;7(8( z-Pu_8J+%+Nr?SW|oGR@u6RP*{42NGx`8~9bPOM=! z);6Jz2d$^jIE~gdp&p?fmMApNgm@1cuh0aICYX>&=qnnnZ$gp>O;l)ojW#eLnb6A` zZD>M@2Tf9F1C2H^p)sM2ma2lAn9$UNCM&d|Mw^+?oY2V{4VuuxgQh67kw#mZ(2CI0 z8f|St8xPu8p-nW})`WJ1)?6k^_ob)Z-6pj6piLFpOrsr4=;+C7=H@vg)bmNr6+6pB zu~2hOcQT=~hsJ+w_*oXzc{q#Q#gi9Qc`bBaR};E<@>;08mYiqfXMcAS?vaV%;Bxg4 z-D`sP8Q)5=t@S#3n$U~TnHs&%gx;RjwozzXjoxoUA3`r`w66*MJZL+G-mTI8CJZ1n z?p0OrKobUe(Dn-LpwUzl1{0d8(KHi=c+idt?WECk6EX-rrqKsXc+i7(R%jQEK4ikf zgjRS>75s<^Lp^9$g?7{EFcTgnbc9BSn=rzIc30><8XalEV}$O~C{AWS?m_QWXb+7( zVZtav3%;%j9&N&t9<--IdujA36P_k?kVc;|VT=d8PocdvI@W}7gl^F2coRY%^nQi* z(P*X#6J*}}H&ngPnlRBr_Els*O+IJBBwA@VjXrO}3m&w;LI-H{MH5~kbeTpcn=r+L z4pitMjZQUT8ljFxr<*XtgQhBUutsN^FpJROo9e#KHerqjO;hL)jm|Y;9-%WeI^Tq_ z2TfOKhDH~d@G_wnHTsGP3q9xq3Vl$ci%eKdXxv+>;3Xz3^`H+a^kI!IGhsQQnHqi7 zgx5UiBMKd=(br9QgV19d#X0&nJ?JonKC02TOjto^g%zscl_sq6pu-h9LZhoqSVQOt zjjlCeod+GM(8n~o-h>T=?$YQ+6E=C!#})d7MmL-AHlYPqs)Dze@Qw!^rO?qD%`#yt zp@TH~t_kmX&?go8lt$k-VH=?vG`ih{9Uk;)g+8OvohIy(dGl9^qJ^Aw>XY%Wy#hrW z+#3Rf4$$aN zCS-fi`3end6nByQOz1j|;{K3dJm><2zN}H)N%AY9w>A2k3BP;LR}{KXqqyhf4??@F zSNHW#6K;6WMG9T4(VHgRB6NvHZ<}z(gDz3%QjO*dKtw=(nZ2gb$N(79$~BR&Orgs) zihEfEq0Khn7jwCnWsVbYcdXz%uTRDP0GKKc1pes`m-$YBcCSkA=QaLG-gX~W`ymf27oD=i^|5`ke z(%2~zK!Mos^i85drLqz#i*z{|l*6FB49a6rK?W59S!*_l+mYe;9wI)xYKy3nb!Us1 z8j;_rio;jUcB-LN3s24xRm0n}M4j-n@xFxcl`K&?+08}ogIPju-;;10CvjW&OrEFZ}Dbm0qhF2!i}?A zZ2;Iq{nPHeI__xrTYoS0_v!rovj5i}rUSJ2L%sN;zx6++{y}$nuWY%)X#pGx&<-b6 zhTP+{3%Ata0330bxCO~2dvM87+&AG}@&PV69)J_!;YWzSpYuwF8K(Td=Y@t?h?0n$$X4U&ID*5*Sf#&q2s+X!qUu{{>< zyIYixc^tzhu#LhtIx76sZc!<`p^xYt8Hx(es^d#2nu!aV;CM~3HN(~%+k~u+d&GSa zkQroM zKio8o4@ZOx#raCbOviWzwwc&wMTOtqD@J!Hhr8j{V_)r#yIBl2MByH?4N>?C-zcMv zGWtjR=r+lvo1)aNx$w^eL`tR2(%g*Z+cJ0?gDo=Hg26j7cqc0S{yxz#M(%vOwJ7gc zG&)qH4;>qlcPtuhjJ#vf@R*dmW6^LU!-pg5@Mh$T>?D zo?`P8mQPteWBHur3zpL?XIRd%oMSo9a)IR{%O#dCS-xWVn&q1i>$fc5v3$?+1IuNW zD=a^vFdqOB_o)O9D$GOMR9kmIf@zEDc#wSQ@c3W@*CGl%*L^IKYB|p1g`GuaeFiWI?LT3)eX*N+;8f z>cMz&kQ|#DV|kMv2URqjX2B4G$05u=K}feC!?WJ13azHm2P}Ay(8C&i$byGG=v@k} zuF*#<7)ogQQ);clEO^v|)=+3ojSjb91fjzIqah1230<$z2^Ku- zK@$~PU!xN(c#hCJ8l7ap^By!wp$#UmQgkIO^EDL6P(8daFqR}}P%q6t>XR6?N7R>jcO%>WqqhSjc5IRGn zFI(`62W_sgWjvq9vc0?g1v;^(da%4_IuEt3hkxQ z0~UNpXxB69zJ7!s`X2N?h4$9yK?@EMx)f3V=s1iYcR4;fFqSvzanMJ@M=Ur>@O8c3 zV-_6uthcX1`)Txq1t$q@epZx@rH`yr7JQNe4Uaf0V)ogvuLe?d1oweW`SyAT{c;12wWW{Uiq6L@8x?fvgTJRNF6SVcU1>cahQd{3z@Euvl zwe`IPKall@wk}(6g{+F_RS`d0aFwj~+PY@Jb+Sfk3%{5FSubnrCkwL4+O4ghE%=43 zAGGzW1;3G1=z?0~?-u+)R*JU%wBQC=soJ_}!7Z|0($;MY?vS-nTls8=u;tf{FSHeD zgF#lrMNyiMvJ^J>q?KcDQZ|fop9xM;_2ugS)j3D-R{^8-v(ayZN#%FP^7#oCupx@9 zr?q9-V3V~RE&iqyZ9{&~oqSq7-Jj7bh_Rsnp*Jomw4e=zJm?sOj@4*k8;TIx@k@oq z+ECPkj#KD(jTW<^IH3zQTEd2s9yFxTOpTVZp){daG+M@nvL1ATLZ8)WIUC9o+W0F~ za0MGGdeDgqeNLm5Y^Y4=6h!&msEQ4fa=aU1EN{}|;CT&KwV@ipXY_jSvZ1S(Eb{2XT#kdG_24C8f|Yw2SV3sw4)83 zJm|{`eMO_4ZRkSiEsb`yp_>O?sL(|k?QX+8gm(T;-Pe0<=;1*ZD|CrQd)m;8(8U_P z&xYO}bg4p@Y4m;@`Ve|mqkV1Y=Rub%^i_@aw_yOGO}|$K542&B2YpSUuWK~bhQWkR zLzLh6(`*>xLEmsur>c76|Ci@YDi(TEuOi)sH*>rLU@ULaxXtMi80@Mw;_aJS3qTheeF*3s%mUKYg*Id#;A{-1Q-r=5j; zMGLRjcV>hQ>#5|0mql4Vuw$eR8**$hjO9&w9BkC^V>WEek%_UqNsog~I`eTGHs#2~ zSl*<^!Dh;gmQyoN*sz%@YX74s%b!4_Y>dLhxdQ`D?_JHoQaD z0c|~HLl#+}t*33+N>=HsYOT-M@UFD@>ywP-O=&i~r`I~h#*Td24ZkXi7k28a$35@N zTI!hm|8Zy5ifx3yMpBQ{SR2OWct!K${J#6_ayqEA|Dw}y@hkoR>@-|C|BY~Z(ni7` zap{dX-iDB8BW_b0v6tG2d~m+*4`f zoz5%obY67IlDyM-<(Ad)mS#>(ETp!@;-4E~` z`eSs04bOT$z;~z*@C|C);DJ5k>CGhXOkg?A1jbuhoe3=0mfq=Z>79B@Pqg7V&z9b) zw)8Hwr5ER&M2t5bzC{1)Pa<}+1x&K#0WNr+ad_T_7j)-E8(z|#$u|B}#61oMe=RbF z7T|7*sW{QBj)R$I;}1LBoawd`8B@#}Kp$Bo~RGd^zorks&+EOnjlF-f6=wW$(6OkBUFA zVXun!*|6X45aM&!9kAg;wcsNgK34HT8xE=Xunk94eAI?xDn4$*2^F8T;gpI$vEfq{ ze`dqyD*nQT(<(j_vf-@ioU{M=FQ+K z?pefl-@neOmjZvCR8K;5R98Ezb9Rms>&=3%?RM}Da-I|Hg)%e8xF^_ao2X8(7l|j> z`_{%IzP_`6F9Cd(z*zO*eQ(1L>JDGFA@>ny`h0)<`?B0854>XI+0*2KNOR#wn+`;B z&V}erQXb=T70bJ3!*yN)j;)vMjLHVE;V18slei??hM(OfzYqhz+H$<#aJ=7fww|t? z@P{pbrxqK4KWxA|fPe9qZGYlKd&qzrHr&Kq^m6`CjZaG`Pw+O=eNgFY3di7)DZNP$OpVQkk zUo>|9+NK!GoAfw{h*n!aG8!VH|H{Nz-lWHYp)#XDM1vty!;@ot?yRgo8hklujO9&w z9GHrRKs1;HpGTNyWvyt4A}dfpjTapac8>8dmN)5fkYA4%6Ak$Z?x*2`(NG`$14^MMRSaYvAju-gW`I;646kc;JO8UIO+fI?On;c zDg{me{)4Mh$h{Lyyn843DKGa_$eYrJd7IZO)yo+1pUEG3p z|L0xYLY=of@8T9}XLR1hE%GjIfnPuKE^d)`aSQ$eop*5y^&ye#;ud&dfShf@(-p}p zT&S}UbvJMtsEE6>g8m)LdAcG;pRMSGKYa0CQMwym++i-xOtr%K8hvqxol)3T^HUg| z{9FGwnGPl0f42X@oLjheZC^rksp!AXNE5ErN7RWa9UX)+*veun7aiVR+gB~zY?`PN zE?vi0!Y|JmGd?P7T_5pkq^~-r)Cga!=PO>aCWi7fF6#6x^709_(5oFT80Rb9xekVP zvDL#Chbb8T zA{v8;=b_%AM6@|St<$>6>4%B^Bi*1!bcS%XjFl6OIIzOrxLNnG$8 z1$h?~&%2;_-UY?+%FlTh6vuaiybFr|-@c%@ykgSAe|R=F_xeb9rSJODc%HTTxFaA* x21(K3Uj~SVE%=&Ue95m^KEGsx=#Fv@JDH^+i4!gNk*SM%(f&uenpbA#{{lq1%_;x@ diff --git a/server/C#/HoneyBox/.vs/HoneyBox/v18/.suo b/server/C#/HoneyBox/.vs/HoneyBox/v18/.suo index 8a6b792c6bfc3b6badd2ce3e19152e28d428b268..ad3a6e4e3693ce58826ecb0c5705bcaa3e7487be 100644 GIT binary patch delta 4494 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 S|2x4dr-Mn{8%FA8f$5* /// -/// 提供系统配置、商品类型、平台配置等功能 +/// 提供系统配置、商品类型、平台配置、悬浮球配置等功能 /// [ApiController] [Route("api")] public class ConfigController : ControllerBase { private readonly IConfigService _configService; + private readonly IFloatBallService _floatBallService; private readonly ILogger _logger; public ConfigController( IConfigService configService, + IFloatBallService floatBallService, ILogger logger) { _configService = configService; + _floatBallService = floatBallService; _logger = logger; } @@ -160,9 +165,9 @@ public class ConfigController : ControllerBase /// /// 请求参数 /// 单页内容数据 - [HttpPost("danye")] + [HttpGet("danye")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> GetDanyeContent([FromBody] DanyeRequest? request) + public async Task> GetDanyeContent([FromQuery] DanyeRequest? request) { try { @@ -184,4 +189,32 @@ public class ConfigController : ControllerBase return ApiResponse.Fail("获取单页内容失败"); } } + + /// + /// 获取悬浮球配置 + /// + /// + /// GET /api/getFloatBall + /// + /// 返回所有启用的悬浮球配置列表 + /// 支持未登录访问 + /// Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 5.1, 5.2, 5.3 + /// + /// 悬浮球配置列表 + [HttpGet("getFloatBall")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetFloatBall() + { + try + { + var result = await _floatBallService.GetEnabledFloatBallsAsync(); + return ApiResponse>.Success(result, "获取悬浮球配置成功"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get float ball configurations"); + return ApiResponse>.Fail("获取悬浮球配置失败"); + } + } } diff --git a/server/C#/HoneyBox/src/HoneyBox.Api/Controllers/WarehouseController.cs b/server/C#/HoneyBox/src/HoneyBox.Api/Controllers/WarehouseController.cs index 94ff5924..f9cc2897 100644 --- a/server/C#/HoneyBox/src/HoneyBox.Api/Controllers/WarehouseController.cs +++ b/server/C#/HoneyBox/src/HoneyBox.Api/Controllers/WarehouseController.cs @@ -1,4 +1,4 @@ -using System.Security.Claims; +using System.Security.Claims; using HoneyBox.Core.Interfaces; using HoneyBox.Model.Base; using HoneyBox.Model.Models; diff --git a/server/C#/HoneyBox/src/HoneyBox.Core/Interfaces/IFloatBallService.cs b/server/C#/HoneyBox/src/HoneyBox.Core/Interfaces/IFloatBallService.cs new file mode 100644 index 00000000..143de586 --- /dev/null +++ b/server/C#/HoneyBox/src/HoneyBox.Core/Interfaces/IFloatBallService.cs @@ -0,0 +1,15 @@ +using HoneyBox.Model.Models.FloatBall; + +namespace HoneyBox.Core.Interfaces; + +/// +/// 悬浮球服务接口 +/// +public interface IFloatBallService +{ + /// + /// 获取所有启用的悬浮球配置 + /// + /// 启用的悬浮球配置列表 + Task> GetEnabledFloatBallsAsync(); +} diff --git a/server/C#/HoneyBox/src/HoneyBox.Core/Services/FloatBallService.cs b/server/C#/HoneyBox/src/HoneyBox.Core/Services/FloatBallService.cs new file mode 100644 index 00000000..cd4c285a --- /dev/null +++ b/server/C#/HoneyBox/src/HoneyBox.Core/Services/FloatBallService.cs @@ -0,0 +1,61 @@ +using HoneyBox.Core.Interfaces; +using HoneyBox.Model.Data; +using HoneyBox.Model.Models.FloatBall; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace HoneyBox.Core.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> GetEnabledFloatBallsAsync() + { + try + { + var floatBalls = await _dbContext.FloatBallConfigs + .Where(f => f.Status == 1) + .Select(f => new FloatBallResponse + { + Id = f.Id, + Type = f.Type, + Image = f.Image, + LinkUrl = f.LinkUrl, + PositionX = f.PositionX, + PositionY = f.PositionY, + Width = f.Width, + Height = f.Height, + Effect = f.Effect, + Title = f.Title, + ImageDetails = f.ImageDetails, + ImageBj = f.ImageBj, + ImageDetailsX = f.ImageDetailsX, + ImageDetailsY = f.ImageDetailsY, + ImageDetailsW = f.ImageDetailsW, + ImageDetailsH = f.ImageDetailsH + }) + .ToListAsync(); + + return floatBalls; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get enabled float ball configurations"); + throw; + } + } +} diff --git a/server/C#/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs b/server/C#/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs index 4ac9146b..42b356e2 100644 --- a/server/C#/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs +++ b/server/C#/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs @@ -277,6 +277,14 @@ public class ServiceModule : Module return new ConfigService(dbContext, logger, redisService); }).As().InstancePerLifetimeScope(); + // 注册悬浮球服务 + builder.Register(c => + { + var dbContext = c.Resolve(); + var logger = c.Resolve>(); + return new FloatBallService(dbContext, logger); + }).As().InstancePerLifetimeScope(); + // ========== 签到系统服务注册 ========== // 注册签到服务 diff --git a/server/C#/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs b/server/C#/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs index 6f9c9d2c..fcffca5a 100644 --- a/server/C#/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs +++ b/server/C#/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs @@ -108,6 +108,8 @@ public partial class HoneyBoxDbContext : DbContext public virtual DbSet Rewards { get; set; } + public virtual DbSet FloatBallConfigs { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // Connection string is configured in Program.cs via dependency injection @@ -2974,6 +2976,97 @@ public partial class HoneyBoxDbContext : DbContext .HasColumnName("updated_at"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("pk_float_ball_configs"); + + entity.ToTable("float_ball_configs", tb => tb.HasComment("悬浮球配置表,存储首页悬浮球配置信息")); + + entity.HasIndex(e => e.Status, "ix_float_ball_configs_status"); + + entity.Property(e => e.Id) + .HasComment("主键ID") + .HasColumnName("id"); + entity.Property(e => e.Status) + .HasDefaultValue((byte)0) + .HasComment("状态: 0关闭 1开启") + .HasColumnName("status"); + entity.Property(e => e.Type) + .HasDefaultValue((byte)1) + .HasComment("类型: 1展示图片 2跳转页面") + .HasColumnName("type"); + entity.Property(e => e.Image) + .HasMaxLength(255) + .HasDefaultValue("") + .HasComment("悬浮球图片URL") + .HasColumnName("image"); + entity.Property(e => e.LinkUrl) + .HasMaxLength(255) + .HasDefaultValue("") + .HasComment("跳转链接") + .HasColumnName("link_url"); + entity.Property(e => e.PositionX) + .HasMaxLength(30) + .HasDefaultValue("") + .HasComment("X轴位置") + .HasColumnName("position_x"); + entity.Property(e => e.PositionY) + .HasMaxLength(30) + .HasDefaultValue("") + .HasComment("Y轴位置") + .HasColumnName("position_y"); + entity.Property(e => e.Width) + .HasMaxLength(30) + .HasDefaultValue("") + .HasComment("宽度") + .HasColumnName("width"); + entity.Property(e => e.Height) + .HasMaxLength(30) + .HasDefaultValue("") + .HasComment("高度") + .HasColumnName("height"); + entity.Property(e => e.Effect) + .HasDefaultValue((byte)0) + .HasComment("特效: 0无 1缩放动画") + .HasColumnName("effect"); + entity.Property(e => e.Title) + .HasMaxLength(255) + .HasComment("标题") + .HasColumnName("title"); + entity.Property(e => e.ImageDetails) + .HasMaxLength(255) + .HasComment("详情图片URL") + .HasColumnName("image_details"); + entity.Property(e => e.ImageBj) + .HasMaxLength(255) + .HasComment("背景图片URL") + .HasColumnName("image_bj"); + entity.Property(e => e.ImageDetailsX) + .HasMaxLength(255) + .HasComment("详情图片X偏移") + .HasColumnName("image_details_x"); + entity.Property(e => e.ImageDetailsY) + .HasMaxLength(255) + .HasComment("详情图片Y偏移") + .HasColumnName("image_details_y"); + entity.Property(e => e.ImageDetailsW) + .HasMaxLength(255) + .HasComment("详情图片宽度") + .HasColumnName("image_details_w"); + entity.Property(e => e.ImageDetailsH) + .HasMaxLength(255) + .HasComment("详情图片高度") + .HasColumnName("image_details_h"); + entity.Property(e => e.CreatedAt) + .HasDefaultValueSql("(getdate())") + .HasComment("创建时间") + .HasColumnName("created_at"); + entity.Property(e => e.UpdatedAt) + .HasDefaultValueSql("(getdate())") + .HasComment("更新时间") + .HasColumnName("updated_at"); + }); + OnModelCreatingPartial(modelBuilder); } diff --git a/server/C#/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs b/server/C#/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs new file mode 100644 index 00000000..2eb6a5a6 --- /dev/null +++ b/server/C#/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs @@ -0,0 +1,104 @@ +using System; + +namespace HoneyBox.Model.Entities; + +/// +/// 悬浮球配置表,存储首页悬浮球配置信息 +/// +public partial class FloatBallConfig +{ + /// + /// 主键ID + /// + public int Id { get; set; } + + /// + /// 状态: 0关闭 1开启 + /// + public byte Status { get; set; } + + /// + /// 类型: 1展示图片 2跳转页面 + /// + public byte Type { get; set; } + + /// + /// 悬浮球图片URL + /// + public string Image { get; set; } = string.Empty; + + /// + /// 跳转链接 + /// + 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; + + /// + /// 特效: 0无 1缩放动画 + /// + public byte Effect { get; set; } + + /// + /// 标题 + /// + public string? Title { get; set; } + + /// + /// 详情图片URL + /// + public string? ImageDetails { get; set; } + + /// + /// 背景图片URL + /// + public string? ImageBj { get; set; } + + /// + /// 详情图片X偏移 + /// + public string? ImageDetailsX { get; set; } + + /// + /// 详情图片Y偏移 + /// + 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; } +} diff --git a/server/C#/HoneyBox/src/HoneyBox.Model/Models/FloatBall/FloatBallResponse.cs b/server/C#/HoneyBox/src/HoneyBox.Model/Models/FloatBall/FloatBallResponse.cs new file mode 100644 index 00000000..f2305d5c --- /dev/null +++ b/server/C#/HoneyBox/src/HoneyBox.Model/Models/FloatBall/FloatBallResponse.cs @@ -0,0 +1,106 @@ +using System.Text.Json.Serialization; + +namespace HoneyBox.Model.Models.FloatBall; + +/// +/// 悬浮球配置响应DTO +/// 排除 status, created_at, updated_at 字段 +/// +public class FloatBallResponse +{ + /// + /// 主键ID + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// 类型: 1展示图片 2跳转页面 + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// 悬浮球图片URL + /// + [JsonPropertyName("image")] + public string Image { get; set; } = string.Empty; + + /// + /// 跳转链接 + /// + [JsonPropertyName("link_url")] + public string LinkUrl { get; set; } = string.Empty; + + /// + /// X轴位置 + /// + [JsonPropertyName("position_x")] + public string PositionX { get; set; } = string.Empty; + + /// + /// Y轴位置 + /// + [JsonPropertyName("position_y")] + public string PositionY { get; set; } = string.Empty; + + /// + /// 宽度 + /// + [JsonPropertyName("width")] + public string Width { get; set; } = string.Empty; + + /// + /// 高度 + /// + [JsonPropertyName("height")] + public string Height { get; set; } = string.Empty; + + /// + /// 特效: 0无 1缩放动画 + /// + [JsonPropertyName("effect")] + public int Effect { get; set; } + + /// + /// 标题 + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// 详情图片URL + /// + [JsonPropertyName("image_details")] + public string? ImageDetails { get; set; } + + /// + /// 背景图片URL + /// + [JsonPropertyName("image_bj")] + public string? ImageBj { get; set; } + + /// + /// 详情图片X偏移 + /// + [JsonPropertyName("image_details_x")] + public string? ImageDetailsX { get; set; } + + /// + /// 详情图片Y偏移 + /// + [JsonPropertyName("image_details_y")] + public string? ImageDetailsY { get; set; } + + /// + /// 详情图片宽度 + /// + [JsonPropertyName("image_details_w")] + public string? ImageDetailsW { get; set; } + + /// + /// 详情图片高度 + /// + [JsonPropertyName("image_details_h")] + public string? ImageDetailsH { get; set; } +} diff --git a/server/scripts/migrate_float_ball.js b/server/scripts/migrate_float_ball.js new file mode 100644 index 00000000..95aaee9b --- /dev/null +++ b/server/scripts/migrate_float_ball.js @@ -0,0 +1,267 @@ +/** + * 悬浮球配置数据迁移脚本 - Node.js + * Feature: float-ball-migration, Property 1: Data Migration Record Count Consistency + * Feature: float-ball-migration, Property 2: Data Migration ID Preservation + * Feature: float-ball-migration, Property 3: Timestamp Transformation Validity + * Feature: float-ball-migration, Property 8: Incremental Migration Idempotence + * Validates: Requirements 2.1, 2.2, 2.3, 2.5, 2.6 + * + * 源表: MySQL float_ball_config + * 目标表: SQL Server float_ball_configs + */ + +const mysql = require('mysql2/promise'); +const sql = require('mssql'); + +// MySQL 配置 +const mysqlConfig = { + host: '192.168.195.16', + port: 1887, + user: 'root', + password: 'Dbt@com@123', + database: 'youdas', + charset: 'utf8mb4' +}; + +// SQL Server 配置 +const sqlServerConfig = { + server: '192.168.195.15', + port: 1433, + user: 'sa', + password: 'Dbt@com@123', + database: 'honey_box', + options: { + encrypt: false, + trustServerCertificate: true + } +}; + +// Unix时间戳转换为 SQL Server DATETIME2 格式 +function unixToDatetime(timestamp) { + if (!timestamp || timestamp === 0) { + return null; + } + const date = new Date(timestamp * 1000); + return date.toISOString().slice(0, 23).replace('T', ' '); +} + +// 转义SQL字符串 +function escapeString(str) { + if (str === null || str === undefined) return 'NULL'; + return "N'" + String(str).replace(/'/g, "''") + "'"; +} + +// 格式化日期时间 +function formatDatetime(dt) { + if (!dt) return 'NULL'; + return "'" + dt + "'"; +} + + +// 获取已迁移的悬浮球配置ID列表 +async function getMigratedIds(pool) { + const result = await pool.request().query('SELECT id FROM float_ball_configs'); + return new Set(result.recordset.map(r => r.id)); +} + +// 批量插入悬浮球配置数据 +async function insertFloatBallsBatch(pool, floatBalls) { + if (floatBalls.length === 0) return 0; + + let insertedCount = 0; + + // 构建批量插入SQL + let sqlBatch = 'SET IDENTITY_INSERT float_ball_configs ON;\n'; + + for (const fb of floatBalls) { + const createdAt = unixToDatetime(fb.create_time) || new Date().toISOString().slice(0, 23).replace('T', ' '); + const updatedAt = unixToDatetime(fb.update_time) || createdAt; + + sqlBatch += ` +INSERT INTO float_ball_configs ( + id, status, type, image, link_url, position_x, position_y, width, height, effect, + title, image_details, image_bj, image_details_x, image_details_y, image_details_w, image_details_h, + created_at, updated_at +) VALUES ( + ${fb.id}, + ${fb.status || 0}, + ${fb.type || 1}, + ${escapeString(fb.image || '')}, + ${escapeString(fb.link_url || '')}, + ${escapeString(fb.position_x || '')}, + ${escapeString(fb.position_y || '')}, + ${escapeString(fb.width || '')}, + ${escapeString(fb.height || '')}, + ${fb.effect || 0}, + ${escapeString(fb.title)}, + ${escapeString(fb.image_details)}, + ${escapeString(fb.image_bj)}, + ${escapeString(fb.image_details_x)}, + ${escapeString(fb.image_details_y)}, + ${escapeString(fb.image_details_w)}, + ${escapeString(fb.image_details_h)}, + ${formatDatetime(createdAt)}, + ${formatDatetime(updatedAt)} +); +`; + } + + sqlBatch += 'SET IDENTITY_INSERT float_ball_configs OFF;'; + + try { + await pool.request().batch(sqlBatch); + insertedCount = floatBalls.length; + } catch (err) { + console.error('批量插入失败:', err.message); + // 如果批量失败,尝试逐条插入 + for (const fb of floatBalls) { + try { + const createdAt = unixToDatetime(fb.create_time) || new Date().toISOString().slice(0, 23).replace('T', ' '); + const updatedAt = unixToDatetime(fb.update_time) || createdAt; + + const singleSql = ` +SET IDENTITY_INSERT float_ball_configs ON; +INSERT INTO float_ball_configs ( + id, status, type, image, link_url, position_x, position_y, width, height, effect, + title, image_details, image_bj, image_details_x, image_details_y, image_details_w, image_details_h, + created_at, updated_at +) VALUES ( + ${fb.id}, + ${fb.status || 0}, + ${fb.type || 1}, + ${escapeString(fb.image || '')}, + ${escapeString(fb.link_url || '')}, + ${escapeString(fb.position_x || '')}, + ${escapeString(fb.position_y || '')}, + ${escapeString(fb.width || '')}, + ${escapeString(fb.height || '')}, + ${fb.effect || 0}, + ${escapeString(fb.title)}, + ${escapeString(fb.image_details)}, + ${escapeString(fb.image_bj)}, + ${escapeString(fb.image_details_x)}, + ${escapeString(fb.image_details_y)}, + ${escapeString(fb.image_details_w)}, + ${escapeString(fb.image_details_h)}, + ${formatDatetime(createdAt)}, + ${formatDatetime(updatedAt)} +); +SET IDENTITY_INSERT float_ball_configs OFF;`; + + await pool.request().batch(singleSql); + insertedCount++; + console.log(` ✓ 插入悬浮球配置 ${fb.id} 成功`); + } catch (singleErr) { + console.error(` ✗ 插入悬浮球配置 ${fb.id} 失败:`, singleErr.message); + } + } + } + + return insertedCount; +} + + +async function main() { + console.log('========================================'); + console.log('悬浮球配置数据迁移脚本 - Node.js'); + console.log('========================================\n'); + + let mysqlConn = null; + let sqlPool = null; + + try { + // 连接 MySQL + console.log('正在连接 MySQL...'); + mysqlConn = await mysql.createConnection(mysqlConfig); + console.log('MySQL 连接成功\n'); + + // 连接 SQL Server + console.log('正在连接 SQL Server...'); + sqlPool = await sql.connect(sqlServerConfig); + console.log('SQL Server 连接成功\n'); + + // 获取已迁移的ID(支持增量迁移) + console.log('正在获取已迁移的悬浮球配置ID...'); + const migratedIds = await getMigratedIds(sqlPool); + console.log(`已迁移悬浮球配置数: ${migratedIds.size}\n`); + + // 从 MySQL 获取所有悬浮球配置数据 + console.log('正在从 MySQL 读取悬浮球配置数据...'); + const [rows] = await mysqlConn.execute(` + SELECT id, status, type, image, link_url, position_x, position_y, width, height, effect, + create_time, update_time, title, image_details, image_bj, + image_details_x, image_details_y, image_details_w, image_details_h + FROM float_ball_config + ORDER BY id + `); + console.log(`MySQL 悬浮球配置总数: ${rows.length}\n`); + + // 过滤出未迁移的配置(增量迁移) + const floatBallsToMigrate = rows.filter(fb => !migratedIds.has(fb.id)); + console.log(`待迁移悬浮球配置数: ${floatBallsToMigrate.length}\n`); + + if (floatBallsToMigrate.length === 0) { + console.log('所有悬浮球配置数据已迁移完成!'); + } else { + // 批量迁移(每批50条) + const batchSize = 50; + let totalInserted = 0; + + for (let i = 0; i < floatBallsToMigrate.length; i += batchSize) { + const batch = floatBallsToMigrate.slice(i, i + batchSize); + const inserted = await insertFloatBallsBatch(sqlPool, batch); + totalInserted += inserted; + console.log(`进度: ${Math.min(i + batchSize, floatBallsToMigrate.length)}/${floatBallsToMigrate.length} (本批插入: ${inserted})`); + } + + console.log(`\n迁移完成!共插入 ${totalInserted} 条记录`); + } + + // 验证迁移结果 + console.log('\n========================================'); + console.log('迁移结果验证'); + console.log('========================================'); + + const [mysqlCount] = await mysqlConn.execute('SELECT COUNT(*) as count FROM float_ball_config'); + const sqlResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM float_ball_configs'); + + console.log(`MySQL float_ball_config 表记录数: ${mysqlCount[0].count}`); + console.log(`SQL Server float_ball_configs 表记录数: ${sqlResult.recordset[0].count}`); + + if (mysqlCount[0].count === sqlResult.recordset[0].count) { + console.log('\n✅ 数据迁移完成,记录数一致!'); + } else { + console.log(`\n⚠️ 记录数不一致,差异: ${mysqlCount[0].count - sqlResult.recordset[0].count}`); + } + + // 验证ID保留 + console.log('\n验证ID保留...'); + const [mysqlIds] = await mysqlConn.execute('SELECT id FROM float_ball_config ORDER BY id'); + const sqlIds = await sqlPool.request().query('SELECT id FROM float_ball_configs ORDER BY id'); + + const mysqlIdSet = new Set(mysqlIds.map(r => r.id)); + const sqlIdSet = new Set(sqlIds.recordset.map(r => r.id)); + + let idMismatch = false; + for (const id of mysqlIdSet) { + if (!sqlIdSet.has(id)) { + console.log(` ⚠️ MySQL ID ${id} 未在 SQL Server 中找到`); + idMismatch = true; + } + } + + if (!idMismatch) { + console.log('✅ 所有ID已正确保留!'); + } + + } catch (err) { + console.error('迁移过程中发生错误:', err); + process.exit(1); + } finally { + // 关闭连接 + if (mysqlConn) await mysqlConn.end(); + if (sqlPool) await sqlPool.close(); + } +} + +main();