diff --git a/.github/workflows/nuget_pre.yml b/.github/workflows/nuget_pre.yml
index 96303e6..b0b93fc 100644
--- a/.github/workflows/nuget_pre.yml
+++ b/.github/workflows/nuget_pre.yml
@@ -13,11 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 6.0.x
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
diff --git a/.github/workflows/nuget_prod.yml b/.github/workflows/nuget_prod.yml
index 999568a..d158e4a 100644
--- a/.github/workflows/nuget_prod.yml
+++ b/.github/workflows/nuget_prod.yml
@@ -13,11 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 6.0.x
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 0802805..9a45b93 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -13,11 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 6.0.x
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
diff --git a/.idea/.idea.EasyTool/.idea/.gitignore b/.idea/.idea.EasyTool/.idea/.gitignore
new file mode 100644
index 0000000..2e97252
--- /dev/null
+++ b/.idea/.idea.EasyTool/.idea/.gitignore
@@ -0,0 +1,15 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# Rider 忽略的文件
+/projectSettingsUpdater.xml
+/.idea.EasyTool.iml
+/contentModel.xml
+/modules.xml
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
diff --git a/.idea/.idea.EasyTool/.idea/encodings.xml b/.idea/.idea.EasyTool/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.EasyTool/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.EasyTool/.idea/indexLayout.xml b/.idea/.idea.EasyTool/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.EasyTool/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.EasyTool/.idea/vcs.xml b/.idea/.idea.EasyTool/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/.idea.EasyTool/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.spec-workflow/templates/design-template.md b/.spec-workflow/templates/design-template.md
new file mode 100644
index 0000000..1295d7b
--- /dev/null
+++ b/.spec-workflow/templates/design-template.md
@@ -0,0 +1,96 @@
+# Design Document
+
+## Overview
+
+[High-level description of the feature and its place in the overall system]
+
+## Steering Document Alignment
+
+### Technical Standards (tech.md)
+[How the design follows documented technical patterns and standards]
+
+### Project Structure (structure.md)
+[How the implementation will follow project organization conventions]
+
+## Code Reuse Analysis
+[What existing code will be leveraged, extended, or integrated with this feature]
+
+### Existing Components to Leverage
+- **[Component/Utility Name]**: [How it will be used]
+- **[Service/Helper Name]**: [How it will be extended]
+
+### Integration Points
+- **[Existing System/API]**: [How the new feature will integrate]
+- **[Database/Storage]**: [How data will connect to existing schemas]
+
+## Architecture
+
+[Describe the overall architecture and design patterns used]
+
+### Modular Design Principles
+- **Single File Responsibility**: Each file should handle one specific concern or domain
+- **Component Isolation**: Create small, focused components rather than large monolithic files
+- **Service Layer Separation**: Separate data access, business logic, and presentation layers
+- **Utility Modularity**: Break utilities into focused, single-purpose modules
+
+```mermaid
+graph TD
+ A[Component A] --> B[Component B]
+ B --> C[Component C]
+```
+
+## Components and Interfaces
+
+### Component 1
+- **Purpose:** [What this component does]
+- **Interfaces:** [Public methods/APIs]
+- **Dependencies:** [What it depends on]
+- **Reuses:** [Existing components/utilities it builds upon]
+
+### Component 2
+- **Purpose:** [What this component does]
+- **Interfaces:** [Public methods/APIs]
+- **Dependencies:** [What it depends on]
+- **Reuses:** [Existing components/utilities it builds upon]
+
+## Data Models
+
+### Model 1
+```
+[Define the structure of Model1 in your language]
+- id: [unique identifier type]
+- name: [string/text type]
+- [Additional properties as needed]
+```
+
+### Model 2
+```
+[Define the structure of Model2 in your language]
+- id: [unique identifier type]
+- [Additional properties as needed]
+```
+
+## Error Handling
+
+### Error Scenarios
+1. **Scenario 1:** [Description]
+ - **Handling:** [How to handle]
+ - **User Impact:** [What user sees]
+
+2. **Scenario 2:** [Description]
+ - **Handling:** [How to handle]
+ - **User Impact:** [What user sees]
+
+## Testing Strategy
+
+### Unit Testing
+- [Unit testing approach]
+- [Key components to test]
+
+### Integration Testing
+- [Integration testing approach]
+- [Key flows to test]
+
+### End-to-End Testing
+- [E2E testing approach]
+- [User scenarios to test]
diff --git a/.spec-workflow/templates/product-template.md b/.spec-workflow/templates/product-template.md
new file mode 100644
index 0000000..82e60de
--- /dev/null
+++ b/.spec-workflow/templates/product-template.md
@@ -0,0 +1,51 @@
+# Product Overview
+
+## Product Purpose
+[Describe the core purpose of this product/project. What problem does it solve?]
+
+## Target Users
+[Who are the primary users of this product? What are their needs and pain points?]
+
+## Key Features
+[List the main features that deliver value to users]
+
+1. **Feature 1**: [Description]
+2. **Feature 2**: [Description]
+3. **Feature 3**: [Description]
+
+## Business Objectives
+[What are the business goals this product aims to achieve?]
+
+- [Objective 1]
+- [Objective 2]
+- [Objective 3]
+
+## Success Metrics
+[How will we measure the success of this product?]
+
+- [Metric 1]: [Target]
+- [Metric 2]: [Target]
+- [Metric 3]: [Target]
+
+## Product Principles
+[Core principles that guide product decisions]
+
+1. **[Principle 1]**: [Explanation]
+2. **[Principle 2]**: [Explanation]
+3. **[Principle 3]**: [Explanation]
+
+## Monitoring & Visibility (if applicable)
+[How do users track progress and monitor the system?]
+
+- **Dashboard Type**: [e.g., Web-based, CLI, Desktop app]
+- **Real-time Updates**: [e.g., WebSocket, polling, push notifications]
+- **Key Metrics Displayed**: [What information is most important to surface]
+- **Sharing Capabilities**: [e.g., read-only links, exports, reports]
+
+## Future Vision
+[Where do we see this product evolving in the future?]
+
+### Potential Enhancements
+- **Remote Access**: [e.g., Tunnel features for sharing dashboards with stakeholders]
+- **Analytics**: [e.g., Historical trends, performance metrics]
+- **Collaboration**: [e.g., Multi-user support, commenting]
diff --git a/.spec-workflow/templates/requirements-template.md b/.spec-workflow/templates/requirements-template.md
new file mode 100644
index 0000000..1c80ca0
--- /dev/null
+++ b/.spec-workflow/templates/requirements-template.md
@@ -0,0 +1,50 @@
+# Requirements Document
+
+## Introduction
+
+[Provide a brief overview of the feature, its purpose, and its value to users]
+
+## Alignment with Product Vision
+
+[Explain how this feature supports the goals outlined in product.md]
+
+## Requirements
+
+### Requirement 1
+
+**User Story:** As a [role], I want [feature], so that [benefit]
+
+#### Acceptance Criteria
+
+1. WHEN [event] THEN [system] SHALL [response]
+2. IF [precondition] THEN [system] SHALL [response]
+3. WHEN [event] AND [condition] THEN [system] SHALL [response]
+
+### Requirement 2
+
+**User Story:** As a [role], I want [feature], so that [benefit]
+
+#### Acceptance Criteria
+
+1. WHEN [event] THEN [system] SHALL [response]
+2. IF [precondition] THEN [system] SHALL [response]
+
+## Non-Functional Requirements
+
+### Code Architecture and Modularity
+- **Single Responsibility Principle**: Each file should have a single, well-defined purpose
+- **Modular Design**: Components, utilities, and services should be isolated and reusable
+- **Dependency Management**: Minimize interdependencies between modules
+- **Clear Interfaces**: Define clean contracts between components and layers
+
+### Performance
+- [Performance requirements]
+
+### Security
+- [Security requirements]
+
+### Reliability
+- [Reliability requirements]
+
+### Usability
+- [Usability requirements]
diff --git a/.spec-workflow/templates/structure-template.md b/.spec-workflow/templates/structure-template.md
new file mode 100644
index 0000000..1ab1fbc
--- /dev/null
+++ b/.spec-workflow/templates/structure-template.md
@@ -0,0 +1,145 @@
+# Project Structure
+
+## Directory Organization
+
+```
+[Define your project's directory structure. Examples below - adapt to your project type]
+
+Example for a library/package:
+project-root/
+├── src/ # Source code
+├── tests/ # Test files
+├── docs/ # Documentation
+├── examples/ # Usage examples
+└── [build/dist/out] # Build output
+
+Example for an application:
+project-root/
+├── [src/app/lib] # Main source code
+├── [assets/resources] # Static resources
+├── [config/settings] # Configuration
+├── [scripts/tools] # Build/utility scripts
+└── [tests/spec] # Test files
+
+Common patterns:
+- Group by feature/module
+- Group by layer (UI, business logic, data)
+- Group by type (models, controllers, views)
+- Flat structure for simple projects
+```
+
+## Naming Conventions
+
+### Files
+- **Components/Modules**: [e.g., `PascalCase`, `snake_case`, `kebab-case`]
+- **Services/Handlers**: [e.g., `UserService`, `user_service`, `user-service`]
+- **Utilities/Helpers**: [e.g., `dateUtils`, `date_utils`, `date-utils`]
+- **Tests**: [e.g., `[filename]_test`, `[filename].test`, `[filename]Test`]
+
+### Code
+- **Classes/Types**: [e.g., `PascalCase`, `CamelCase`, `snake_case`]
+- **Functions/Methods**: [e.g., `camelCase`, `snake_case`, `PascalCase`]
+- **Constants**: [e.g., `UPPER_SNAKE_CASE`, `SCREAMING_CASE`, `PascalCase`]
+- **Variables**: [e.g., `camelCase`, `snake_case`, `lowercase`]
+
+## Import Patterns
+
+### Import Order
+1. External dependencies
+2. Internal modules
+3. Relative imports
+4. Style imports
+
+### Module/Package Organization
+```
+[Describe your project's import/include patterns]
+Examples:
+- Absolute imports from project root
+- Relative imports within modules
+- Package/namespace organization
+- Dependency management approach
+```
+
+## Code Structure Patterns
+
+[Define common patterns for organizing code within files. Below are examples - choose what applies to your project]
+
+### Module/Class Organization
+```
+Example patterns:
+1. Imports/includes/dependencies
+2. Constants and configuration
+3. Type/interface definitions
+4. Main implementation
+5. Helper/utility functions
+6. Exports/public API
+```
+
+### Function/Method Organization
+```
+Example patterns:
+- Input validation first
+- Core logic in the middle
+- Error handling throughout
+- Clear return points
+```
+
+### File Organization Principles
+```
+Choose what works for your project:
+- One class/module per file
+- Related functionality grouped together
+- Public API at the top/bottom
+- Implementation details hidden
+```
+
+## Code Organization Principles
+
+1. **Single Responsibility**: Each file should have one clear purpose
+2. **Modularity**: Code should be organized into reusable modules
+3. **Testability**: Structure code to be easily testable
+4. **Consistency**: Follow patterns established in the codebase
+
+## Module Boundaries
+[Define how different parts of your project interact and maintain separation of concerns]
+
+Examples of boundary patterns:
+- **Core vs Plugins**: Core functionality vs extensible plugins
+- **Public API vs Internal**: What's exposed vs implementation details
+- **Platform-specific vs Cross-platform**: OS-specific code isolation
+- **Stable vs Experimental**: Production code vs experimental features
+- **Dependencies direction**: Which modules can depend on which
+
+## Code Size Guidelines
+[Define your project's guidelines for file and function sizes]
+
+Suggested guidelines:
+- **File size**: [Define maximum lines per file]
+- **Function/Method size**: [Define maximum lines per function]
+- **Class/Module complexity**: [Define complexity limits]
+- **Nesting depth**: [Maximum nesting levels]
+
+## Dashboard/Monitoring Structure (if applicable)
+[How dashboard or monitoring components are organized]
+
+### Example Structure:
+```
+src/
+└── dashboard/ # Self-contained dashboard subsystem
+ ├── server/ # Backend server components
+ ├── client/ # Frontend assets
+ ├── shared/ # Shared types/utilities
+ └── public/ # Static assets
+```
+
+### Separation of Concerns
+- Dashboard isolated from core business logic
+- Own CLI entry point for independent operation
+- Minimal dependencies on main application
+- Can be disabled without affecting core functionality
+
+## Documentation Standards
+- All public APIs must have documentation
+- Complex logic should include inline comments
+- README files for major modules
+- Follow language-specific documentation conventions
diff --git a/.spec-workflow/templates/tasks-template.md b/.spec-workflow/templates/tasks-template.md
new file mode 100644
index 0000000..be461de
--- /dev/null
+++ b/.spec-workflow/templates/tasks-template.md
@@ -0,0 +1,139 @@
+# Tasks Document
+
+- [ ] 1. Create core interfaces in src/types/feature.ts
+ - File: src/types/feature.ts
+ - Define TypeScript interfaces for feature data structures
+ - Extend existing base interfaces from base.ts
+ - Purpose: Establish type safety for feature implementation
+ - _Leverage: src/types/base.ts_
+ - _Requirements: 1.1_
+ - _Prompt: Role: TypeScript Developer specializing in type systems and interfaces | Task: Create comprehensive TypeScript interfaces for the feature data structures following requirements 1.1, extending existing base interfaces from src/types/base.ts | Restrictions: Do not modify existing base interfaces, maintain backward compatibility, follow project naming conventions | Success: All interfaces compile without errors, proper inheritance from base types, full type coverage for feature requirements_
+
+- [ ] 2. Create base model class in src/models/FeatureModel.ts
+ - File: src/models/FeatureModel.ts
+ - Implement base model extending BaseModel class
+ - Add validation methods using existing validation utilities
+ - Purpose: Provide data layer foundation for feature
+ - _Leverage: src/models/BaseModel.ts, src/utils/validation.ts_
+ - _Requirements: 2.1_
+ - _Prompt: Role: Backend Developer with expertise in Node.js and data modeling | Task: Create a base model class extending BaseModel and implementing validation following requirement 2.1, leveraging existing patterns from src/models/BaseModel.ts and src/utils/validation.ts | Restrictions: Must follow existing model patterns, do not bypass validation utilities, maintain consistent error handling | Success: Model extends BaseModel correctly, validation methods implemented and tested, follows project architecture patterns_
+
+- [ ] 3. Add specific model methods to FeatureModel.ts
+ - File: src/models/FeatureModel.ts (continue from task 2)
+ - Implement create, update, delete methods
+ - Add relationship handling for foreign keys
+ - Purpose: Complete model functionality for CRUD operations
+ - _Leverage: src/models/BaseModel.ts_
+ - _Requirements: 2.2, 2.3_
+ - _Prompt: Role: Backend Developer with expertise in ORM and database operations | Task: Implement CRUD methods and relationship handling in FeatureModel.ts following requirements 2.2 and 2.3, extending patterns from src/models/BaseModel.ts | Restrictions: Must maintain transaction integrity, follow existing relationship patterns, do not duplicate base model functionality | Success: All CRUD operations work correctly, relationships are properly handled, database operations are atomic and efficient_
+
+- [ ] 4. Create model unit tests in tests/models/FeatureModel.test.ts
+ - File: tests/models/FeatureModel.test.ts
+ - Write tests for model validation and CRUD methods
+ - Use existing test utilities and fixtures
+ - Purpose: Ensure model reliability and catch regressions
+ - _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_
+ - _Requirements: 2.1, 2.2_
+ - _Prompt: Role: QA Engineer with expertise in unit testing and Jest/Mocha frameworks | Task: Create comprehensive unit tests for FeatureModel validation and CRUD methods covering requirements 2.1 and 2.2, using existing test utilities from tests/helpers/testUtils.ts and fixtures from tests/fixtures/data.ts | Restrictions: Must test both success and failure scenarios, do not test external dependencies directly, maintain test isolation | Success: All model methods are tested with good coverage, edge cases covered, tests run independently and consistently_
+
+- [ ] 5. Create service interface in src/services/IFeatureService.ts
+ - File: src/services/IFeatureService.ts
+ - Define service contract with method signatures
+ - Extend base service interface patterns
+ - Purpose: Establish service layer contract for dependency injection
+ - _Leverage: src/services/IBaseService.ts_
+ - _Requirements: 3.1_
+ - _Prompt: Role: Software Architect specializing in service-oriented architecture and TypeScript interfaces | Task: Design service interface contract following requirement 3.1, extending base service patterns from src/services/IBaseService.ts for dependency injection | Restrictions: Must maintain interface segregation principle, do not expose internal implementation details, ensure contract compatibility with DI container | Success: Interface is well-defined with clear method signatures, extends base service appropriately, supports all required service operations_
+
+- [ ] 6. Implement feature service in src/services/FeatureService.ts
+ - File: src/services/FeatureService.ts
+ - Create concrete service implementation using FeatureModel
+ - Add error handling with existing error utilities
+ - Purpose: Provide business logic layer for feature operations
+ - _Leverage: src/services/BaseService.ts, src/utils/errorHandler.ts, src/models/FeatureModel.ts_
+ - _Requirements: 3.2_
+ - _Prompt: Role: Backend Developer with expertise in service layer architecture and business logic | Task: Implement concrete FeatureService following requirement 3.2, using FeatureModel and extending BaseService patterns with proper error handling from src/utils/errorHandler.ts | Restrictions: Must implement interface contract exactly, do not bypass model validation, maintain separation of concerns from data layer | Success: Service implements all interface methods correctly, robust error handling implemented, business logic is well-encapsulated and testable_
+
+- [ ] 7. Add service dependency injection in src/utils/di.ts
+ - File: src/utils/di.ts (modify existing)
+ - Register FeatureService in dependency injection container
+ - Configure service lifetime and dependencies
+ - Purpose: Enable service injection throughout application
+ - _Leverage: existing DI configuration in src/utils/di.ts_
+ - _Requirements: 3.1_
+ - _Prompt: Role: DevOps Engineer with expertise in dependency injection and IoC containers | Task: Register FeatureService in DI container following requirement 3.1, configuring appropriate lifetime and dependencies using existing patterns from src/utils/di.ts | Restrictions: Must follow existing DI container patterns, do not create circular dependencies, maintain service resolution efficiency | Success: FeatureService is properly registered and resolvable, dependencies are correctly configured, service lifetime is appropriate for use case_
+
+- [ ] 8. Create service unit tests in tests/services/FeatureService.test.ts
+ - File: tests/services/FeatureService.test.ts
+ - Write tests for service methods with mocked dependencies
+ - Test error handling scenarios
+ - Purpose: Ensure service reliability and proper error handling
+ - _Leverage: tests/helpers/testUtils.ts, tests/mocks/modelMocks.ts_
+ - _Requirements: 3.2, 3.3_
+ - _Prompt: Role: QA Engineer with expertise in service testing and mocking frameworks | Task: Create comprehensive unit tests for FeatureService methods covering requirements 3.2 and 3.3, using mocked dependencies from tests/mocks/modelMocks.ts and test utilities | Restrictions: Must mock all external dependencies, test business logic in isolation, do not test framework code | Success: All service methods tested with proper mocking, error scenarios covered, tests verify business logic correctness and error handling_
+
+- [ ] 4. Create API endpoints
+ - Design API structure
+ - _Leverage: src/api/baseApi.ts, src/utils/apiUtils.ts_
+ - _Requirements: 4.0_
+ - _Prompt: Role: API Architect specializing in RESTful design and Express.js | Task: Design comprehensive API structure following requirement 4.0, leveraging existing patterns from src/api/baseApi.ts and utilities from src/utils/apiUtils.ts | Restrictions: Must follow REST conventions, maintain API versioning compatibility, do not expose internal data structures directly | Success: API structure is well-designed and documented, follows existing patterns, supports all required operations with proper HTTP methods and status codes_
+
+- [ ] 4.1 Set up routing and middleware
+ - Configure application routes
+ - Add authentication middleware
+ - Set up error handling middleware
+ - _Leverage: src/middleware/auth.ts, src/middleware/errorHandler.ts_
+ - _Requirements: 4.1_
+ - _Prompt: Role: Backend Developer with expertise in Express.js middleware and routing | Task: Configure application routes and middleware following requirement 4.1, integrating authentication from src/middleware/auth.ts and error handling from src/middleware/errorHandler.ts | Restrictions: Must maintain middleware order, do not bypass security middleware, ensure proper error propagation | Success: Routes are properly configured with correct middleware chain, authentication works correctly, errors are handled gracefully throughout the request lifecycle_
+
+- [ ] 4.2 Implement CRUD endpoints
+ - Create API endpoints
+ - Add request validation
+ - Write API integration tests
+ - _Leverage: src/controllers/BaseController.ts, src/utils/validation.ts_
+ - _Requirements: 4.2, 4.3_
+ - _Prompt: Role: Full-stack Developer with expertise in API development and validation | Task: Implement CRUD endpoints following requirements 4.2 and 4.3, extending BaseController patterns and using validation utilities from src/utils/validation.ts | Restrictions: Must validate all inputs, follow existing controller patterns, ensure proper HTTP status codes and responses | Success: All CRUD operations work correctly, request validation prevents invalid data, integration tests pass and cover all endpoints_
+
+- [ ] 5. Add frontend components
+ - Plan component architecture
+ - _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_
+ - _Requirements: 5.0_
+ - _Prompt: Role: Frontend Architect with expertise in React component design and architecture | Task: Plan comprehensive component architecture following requirement 5.0, leveraging base patterns from src/components/BaseComponent.tsx and theme system from src/styles/theme.ts | Restrictions: Must follow existing component patterns, maintain design system consistency, ensure component reusability | Success: Architecture is well-planned and documented, components are properly organized, follows existing patterns and theme system_
+
+- [ ] 5.1 Create base UI components
+ - Set up component structure
+ - Implement reusable components
+ - Add styling and theming
+ - _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_
+ - _Requirements: 5.1_
+ - _Prompt: Role: Frontend Developer specializing in React and component architecture | Task: Create reusable UI components following requirement 5.1, extending BaseComponent patterns and using existing theme system from src/styles/theme.ts | Restrictions: Must use existing theme variables, follow component composition patterns, ensure accessibility compliance | Success: Components are reusable and properly themed, follow existing architecture, accessible and responsive_
+
+- [ ] 5.2 Implement feature-specific components
+ - Create feature components
+ - Add state management
+ - Connect to API endpoints
+ - _Leverage: src/hooks/useApi.ts, src/components/BaseComponent.tsx_
+ - _Requirements: 5.2, 5.3_
+ - _Prompt: Role: React Developer with expertise in state management and API integration | Task: Implement feature-specific components following requirements 5.2 and 5.3, using API hooks from src/hooks/useApi.ts and extending BaseComponent patterns | Restrictions: Must use existing state management patterns, handle loading and error states properly, maintain component performance | Success: Components are fully functional with proper state management, API integration works smoothly, user experience is responsive and intuitive_
+
+- [ ] 6. Integration and testing
+ - Plan integration approach
+ - _Leverage: src/utils/integrationUtils.ts, tests/helpers/testUtils.ts_
+ - _Requirements: 6.0_
+ - _Prompt: Role: Integration Engineer with expertise in system integration and testing strategies | Task: Plan comprehensive integration approach following requirement 6.0, leveraging integration utilities from src/utils/integrationUtils.ts and test helpers | Restrictions: Must consider all system components, ensure proper test coverage, maintain integration test reliability | Success: Integration plan is comprehensive and feasible, all system components work together correctly, integration points are well-tested_
+
+- [ ] 6.1 Write end-to-end tests
+ - Set up E2E testing framework
+ - Write user journey tests
+ - Add test automation
+ - _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_
+ - _Requirements: All_
+ - _Prompt: Role: QA Automation Engineer with expertise in E2E testing and test frameworks like Cypress or Playwright | Task: Implement comprehensive end-to-end tests covering all requirements, setting up testing framework and user journey tests using test utilities and fixtures | Restrictions: Must test real user workflows, ensure tests are maintainable and reliable, do not test implementation details | Success: E2E tests cover all critical user journeys, tests run reliably in CI/CD pipeline, user experience is validated from end-to-end_
+
+- [ ] 6.2 Final integration and cleanup
+ - Integrate all components
+ - Fix any integration issues
+ - Clean up code and documentation
+ - _Leverage: src/utils/cleanup.ts, docs/templates/_
+ - _Requirements: All_
+ - _Prompt: Role: Senior Developer with expertise in code quality and system integration | Task: Complete final integration of all components and perform comprehensive cleanup covering all requirements, using cleanup utilities and documentation templates | Restrictions: Must not break existing functionality, ensure code quality standards are met, maintain documentation consistency | Success: All components are fully integrated and working together, code is clean and well-documented, system meets all requirements and quality standards_
diff --git a/.spec-workflow/templates/tech-template.md b/.spec-workflow/templates/tech-template.md
new file mode 100644
index 0000000..57cd538
--- /dev/null
+++ b/.spec-workflow/templates/tech-template.md
@@ -0,0 +1,99 @@
+# Technology Stack
+
+## Project Type
+[Describe what kind of project this is: web application, CLI tool, desktop application, mobile app, library, API service, embedded system, game, etc.]
+
+## Core Technologies
+
+### Primary Language(s)
+- **Language**: [e.g., Python 3.11, Go 1.21, TypeScript, Rust, C++]
+- **Runtime/Compiler**: [if applicable]
+- **Language-specific tools**: [package managers, build tools, etc.]
+
+### Key Dependencies/Libraries
+[List the main libraries and frameworks your project depends on]
+- **[Library/Framework name]**: [Purpose and version]
+- **[Library/Framework name]**: [Purpose and version]
+
+### Application Architecture
+[Describe how your application is structured - this could be MVC, event-driven, plugin-based, client-server, standalone, microservices, monolithic, etc.]
+
+### Data Storage (if applicable)
+- **Primary storage**: [e.g., PostgreSQL, files, in-memory, cloud storage]
+- **Caching**: [e.g., Redis, in-memory, disk cache]
+- **Data formats**: [e.g., JSON, Protocol Buffers, XML, binary]
+
+### External Integrations (if applicable)
+- **APIs**: [External services you integrate with]
+- **Protocols**: [e.g., HTTP/REST, gRPC, WebSocket, TCP/IP]
+- **Authentication**: [e.g., OAuth, API keys, certificates]
+
+### Monitoring & Dashboard Technologies (if applicable)
+- **Dashboard Framework**: [e.g., React, Vue, vanilla JS, terminal UI]
+- **Real-time Communication**: [e.g., WebSocket, Server-Sent Events, polling]
+- **Visualization Libraries**: [e.g., Chart.js, D3, terminal graphs]
+- **State Management**: [e.g., Redux, Vuex, file system as source of truth]
+
+## Development Environment
+
+### Build & Development Tools
+- **Build System**: [e.g., Make, CMake, Gradle, npm scripts, cargo]
+- **Package Management**: [e.g., pip, npm, cargo, go mod, apt, brew]
+- **Development workflow**: [e.g., hot reload, watch mode, REPL]
+
+### Code Quality Tools
+- **Static Analysis**: [Tools for code quality and correctness]
+- **Formatting**: [Code style enforcement tools]
+- **Testing Framework**: [Unit, integration, and/or end-to-end testing tools]
+- **Documentation**: [Documentation generation tools]
+
+### Version Control & Collaboration
+- **VCS**: [e.g., Git, Mercurial, SVN]
+- **Branching Strategy**: [e.g., Git Flow, GitHub Flow, trunk-based]
+- **Code Review Process**: [How code reviews are conducted]
+
+### Dashboard Development (if applicable)
+- **Live Reload**: [e.g., Hot module replacement, file watchers]
+- **Port Management**: [e.g., Dynamic allocation, configurable ports]
+- **Multi-Instance Support**: [e.g., Running multiple dashboards simultaneously]
+
+## Deployment & Distribution (if applicable)
+- **Target Platform(s)**: [Where/how the project runs: cloud, on-premise, desktop, mobile, embedded]
+- **Distribution Method**: [How users get your software: download, package manager, app store, SaaS]
+- **Installation Requirements**: [Prerequisites, system requirements]
+- **Update Mechanism**: [How updates are delivered]
+
+## Technical Requirements & Constraints
+
+### Performance Requirements
+- [e.g., response time, throughput, memory usage, startup time]
+- [Specific benchmarks or targets]
+
+### Compatibility Requirements
+- **Platform Support**: [Operating systems, architectures, versions]
+- **Dependency Versions**: [Minimum/maximum versions of dependencies]
+- **Standards Compliance**: [Industry standards, protocols, specifications]
+
+### Security & Compliance
+- **Security Requirements**: [Authentication, encryption, data protection]
+- **Compliance Standards**: [GDPR, HIPAA, SOC2, etc. if applicable]
+- **Threat Model**: [Key security considerations]
+
+### Scalability & Reliability
+- **Expected Load**: [Users, requests, data volume]
+- **Availability Requirements**: [Uptime targets, disaster recovery]
+- **Growth Projections**: [How the system needs to scale]
+
+## Technical Decisions & Rationale
+[Document key architectural and technology choices]
+
+### Decision Log
+1. **[Technology/Pattern Choice]**: [Why this was chosen, alternatives considered]
+2. **[Architecture Decision]**: [Rationale, trade-offs accepted]
+3. **[Tool/Library Selection]**: [Reasoning, evaluation criteria]
+
+## Known Limitations
+[Document any technical debt, limitations, or areas for improvement]
+
+- [Limitation 1]: [Impact and potential future solutions]
+- [Limitation 2]: [Why it exists and when it might be addressed]
diff --git a/.spec-workflow/user-templates/README.md b/.spec-workflow/user-templates/README.md
new file mode 100644
index 0000000..ad36a48
--- /dev/null
+++ b/.spec-workflow/user-templates/README.md
@@ -0,0 +1,64 @@
+# User Templates
+
+This directory allows you to create custom templates that override the default Spec Workflow templates.
+
+## How to Use Custom Templates
+
+1. **Create your custom template file** in this directory with the exact same name as the default template you want to override:
+ - `requirements-template.md` - Override requirements document template
+ - `design-template.md` - Override design document template
+ - `tasks-template.md` - Override tasks document template
+ - `product-template.md` - Override product steering template
+ - `tech-template.md` - Override tech steering template
+ - `structure-template.md` - Override structure steering template
+
+2. **Template Loading Priority**:
+ - The system first checks this `user-templates/` directory
+ - If a matching template is found here, it will be used
+ - Otherwise, the default template from `templates/` will be used
+
+## Example Custom Template
+
+To create a custom requirements template:
+
+1. Create a file named `requirements-template.md` in this directory
+2. Add your custom structure, for example:
+
+```markdown
+# Requirements Document
+
+## Executive Summary
+[Your custom section]
+
+## Business Requirements
+[Your custom structure]
+
+## Technical Requirements
+[Your custom fields]
+
+## Custom Sections
+[Add any sections specific to your workflow]
+```
+
+## Template Variables
+
+Templates can include placeholders that will be replaced when documents are created:
+- `{{projectName}}` - The name of your project
+- `{{featureName}}` - The name of the feature being specified
+- `{{date}}` - The current date
+- `{{author}}` - The document author
+
+## Best Practices
+
+1. **Start from defaults**: Copy a default template from `../templates/` as a starting point
+2. **Keep structure consistent**: Maintain similar section headers for tool compatibility
+3. **Document changes**: Add comments explaining why sections were added/modified
+4. **Version control**: Track your custom templates in version control
+5. **Test thoroughly**: Ensure custom templates work with the spec workflow tools
+
+## Notes
+
+- Custom templates are project-specific and not included in the package distribution
+- The `templates/` directory contains the default templates which are updated with each version
+- Your custom templates in this directory are preserved during updates
+- If a custom template has errors, the system will fall back to the default template
diff --git a/EasyTool.Core/AICategory/OpenAIClient.cs b/EasyTool.Core/AICategory/OpenAIClient.cs
new file mode 100644
index 0000000..453a339
--- /dev/null
+++ b/EasyTool.Core/AICategory/OpenAIClient.cs
@@ -0,0 +1,527 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EasyTool.AICategory
+{
+ ///
+ /// OpenAI API 工具类
+ /// 提供 GPT、DALL-E、Whisper 等 AI 服务的集成
+ ///
+ public class OpenAIClient
+ {
+ private readonly string _apiKey;
+ private readonly string _baseUrl;
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// 创建 OpenAI 客户端
+ ///
+ /// API Key
+ /// API 基础 URL(默认 OpenAI 官方)
+ public OpenAIClient(string apiKey, string? baseUrl = null)
+ {
+ _apiKey = apiKey;
+ _baseUrl = baseUrl ?? "https://api.openai.com/v1";
+ _httpClient = new HttpClient
+ {
+ Timeout = TimeSpan.FromMinutes(5)
+ };
+ _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
+ }
+
+ #region Chat Completions
+
+ ///
+ /// 发送聊天请求
+ ///
+ /// 消息列表
+ /// 模型名称
+ /// 温度(0-2)
+ /// 最大令牌数
+ /// 取消令牌
+ /// 响应结果
+ public async Task ChatAsync(List messages, string model = "gpt-3.5-turbo", double temperature = 0.7, int? maxTokens = null, CancellationToken cancellationToken = default)
+ {
+ var requestBody = new Dictionary
+ {
+ ["model"] = model,
+ ["messages"] = messages,
+ ["temperature"] = temperature
+ };
+
+ if (maxTokens.HasValue)
+ requestBody["max_tokens"] = maxTokens.Value;
+
+ var json = JsonSerializer.Serialize(requestBody);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync($"{_baseUrl}/chat/completions", content, cancellationToken);
+ var responseJson = await ReadContentAsStringAsync(response.Content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new OpenAIException($"API 请求失败: {response.StatusCode}", responseJson);
+ }
+
+ return JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ }) ?? throw new OpenAIException("无法解析响应");
+ }
+
+ ///
+ /// 发送简单聊天请求
+ ///
+ /// 提示词
+ /// 模型名称
+ /// 温度
+ /// 取消令牌
+ /// 响应文本
+ public async Task ChatSimpleAsync(string prompt, string model = "gpt-3.5-turbo", double temperature = 0.7, CancellationToken cancellationToken = default)
+ {
+ var messages = new List
+ {
+ new() { Role = "user", Content = prompt }
+ };
+
+ var response = await ChatAsync(messages, model, temperature, cancellationToken: cancellationToken);
+ return response.Choices[0].Message.Content;
+ }
+
+ ///
+ /// 流式聊天请求
+ ///
+ public async IAsyncEnumerable ChatStreamAsync(List messages, string model = "gpt-3.5-turbo", double temperature = 0.7, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var requestBody = new Dictionary
+ {
+ ["model"] = model,
+ ["messages"] = messages,
+ ["temperature"] = temperature,
+ ["stream"] = true
+ };
+
+ var json = JsonSerializer.Serialize(requestBody);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/chat/completions")
+ {
+ Content = content
+ };
+
+ using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ using var stream = await ReadContentAsStreamAsync(response.Content);
+ using var reader = new StreamReader(stream);
+
+ while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
+ {
+ var line = await reader.ReadLineAsync();
+ if (string.IsNullOrEmpty(line) || !line.StartsWith("data: "))
+ continue;
+
+ var data = line.Substring(6);
+ if (data == "[DONE]")
+ break;
+
+ var chunkResponse = JsonSerializer.Deserialize(data, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ if (chunkResponse?.Choices?[0]?.Delta?.Content != null)
+ {
+ yield return chunkResponse.Choices[0].Delta.Content;
+ }
+ }
+ }
+
+ #endregion
+
+ #region Embeddings
+
+ ///
+ /// 获取文本嵌入向量
+ ///
+ /// 文本
+ /// 模型名称
+ /// 取消令牌
+ /// 嵌入向量
+ public async Task GetEmbeddingAsync(string text, string model = "text-embedding-ada-002", CancellationToken cancellationToken = default)
+ {
+ var requestBody = new Dictionary
+ {
+ ["model"] = model,
+ ["input"] = text
+ };
+
+ var json = JsonSerializer.Serialize(requestBody);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync($"{_baseUrl}/embeddings", content, cancellationToken);
+ var responseJson = await ReadContentAsStringAsync(response.Content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new OpenAIException($"API 请求失败: {response.StatusCode}", responseJson);
+ }
+
+ var embeddingResponse = JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ return embeddingResponse?.Data?[0]?.Embedding ?? Array.Empty();
+ }
+
+ ///
+ /// 批量获取嵌入向量
+ ///
+ public async Task> GetEmbeddingsAsync(List texts, string model = "text-embedding-ada-002", CancellationToken cancellationToken = default)
+ {
+ var requestBody = new Dictionary
+ {
+ ["model"] = model,
+ ["input"] = texts
+ };
+
+ var json = JsonSerializer.Serialize(requestBody);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync($"{_baseUrl}/embeddings", content, cancellationToken);
+ var responseJson = await ReadContentAsStringAsync(response.Content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new OpenAIException($"API 请求失败: {response.StatusCode}", responseJson);
+ }
+
+ var embeddingResponse = JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ var result = new List();
+ if (embeddingResponse?.Data != null)
+ {
+ foreach (var item in embeddingResponse.Data)
+ {
+ result.Add(item.Embedding ?? Array.Empty());
+ }
+ }
+
+ return result;
+ }
+
+ #endregion
+
+ #region Image Generation
+
+ ///
+ /// 生成图像
+ ///
+ /// 提示词
+ /// 尺寸(256x256, 512x512, 1024x1024)
+ /// 生成数量
+ /// 取消令牌
+ /// 图像 URL 列表
+ public async Task> GenerateImageAsync(string prompt, string size = "1024x1024", int n = 1, CancellationToken cancellationToken = default)
+ {
+ var requestBody = new Dictionary
+ {
+ ["prompt"] = prompt,
+ ["size"] = size,
+ ["n"] = n
+ };
+
+ var json = JsonSerializer.Serialize(requestBody);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync($"{_baseUrl}/images/generations", content, cancellationToken);
+ var responseJson = await ReadContentAsStringAsync(response.Content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new OpenAIException($"API 请求失败: {response.StatusCode}", responseJson);
+ }
+
+ var imageResponse = JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ var result = new List();
+ if (imageResponse?.Data != null)
+ {
+ foreach (var item in imageResponse.Data)
+ {
+ if (!string.IsNullOrEmpty(item.Url))
+ result.Add(item.Url);
+ }
+ }
+
+ return result;
+ }
+
+ #endregion
+
+ #region Audio
+
+ ///
+ /// 语音转文字
+ ///
+ /// 音频文件路径
+ /// 模型名称
+ /// 语言(如 "zh", "en")
+ /// 取消令牌
+ /// 转录文本
+ public async Task TranscribeAsync(string audioFilePath, string model = "whisper-1", string? language = null, CancellationToken cancellationToken = default)
+ {
+ using var formContent = new MultipartFormDataContent();
+ formContent.Add(new StreamContent(File.OpenRead(audioFilePath)), "file", Path.GetFileName(audioFilePath));
+ formContent.Add(new StringContent(model), "model");
+
+ if (!string.IsNullOrEmpty(language))
+ formContent.Add(new StringContent(language), "language");
+
+ var response = await _httpClient.PostAsync($"{_baseUrl}/audio/transcriptions", formContent, cancellationToken);
+ var responseJson = await ReadContentAsStringAsync(response.Content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new OpenAIException($"API 请求失败: {response.StatusCode}", responseJson);
+ }
+
+ var transcriptionResponse = JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ return transcriptionResponse?.Text ?? string.Empty;
+ }
+
+ ///
+ /// 文字转语音
+ ///
+ /// 文本
+ /// 输出文件路径
+ /// 模型名称
+ /// 声音(alloy, echo, fable, onyx, nova, shimmer)
+ /// 取消令牌
+ /// 是否成功
+ public async Task TextToSpeechAsync(string text, string outputFilePath, string model = "tts-1", string voice = "alloy", CancellationToken cancellationToken = default)
+ {
+ var requestBody = new Dictionary
+ {
+ ["model"] = model,
+ ["input"] = text,
+ ["voice"] = voice
+ };
+
+ var json = JsonSerializer.Serialize(requestBody);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync($"{_baseUrl}/audio/speech", content, cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var errorJson = await ReadContentAsStringAsync(response.Content);
+ throw new OpenAIException($"API 请求失败: {response.StatusCode}", errorJson);
+ }
+
+ var audioData = await ReadContentAsByteArrayAsync(response.Content);
+ await File.WriteAllBytesAsync(outputFilePath, audioData, cancellationToken);
+
+ return true;
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ private static async Task ReadContentAsStringAsync(HttpContent content)
+ {
+#if NETSTANDARD2_1
+ return await content.ReadAsStringAsync();
+#else
+ return await content.ReadAsStringAsync(default);
+#endif
+ }
+
+ private static async Task ReadContentAsStreamAsync(HttpContent content)
+ {
+#if NETSTANDARD2_1
+ return await content.ReadAsStreamAsync();
+#else
+ return await content.ReadAsStreamAsync(default);
+#endif
+ }
+
+ private static async Task ReadContentAsByteArrayAsync(HttpContent content)
+ {
+#if NETSTANDARD2_1
+ return await content.ReadAsByteArrayAsync();
+#else
+ return await content.ReadAsByteArrayAsync(default);
+#endif
+ }
+
+ #endregion
+ }
+
+ #region 数据模型
+
+ ///
+ /// 聊天消息
+ ///
+ public class ChatMessage
+ {
+ ///
+ /// 角色(system, user, assistant)
+ ///
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// 内容
+ ///
+ public string Content { get; set; } = string.Empty;
+ }
+
+ ///
+ /// 聊天响应
+ ///
+ public class ChatResponse
+ {
+ ///
+ /// 响应 ID
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 选择列表
+ ///
+ public List Choices { get; set; } = new();
+
+ ///
+ /// 使用情况
+ ///
+ public UsageInfo? Usage { get; set; }
+ }
+
+ ///
+ /// 聊天选择
+ ///
+ public class ChatChoice
+ {
+ ///
+ /// 索引
+ ///
+ public int Index { get; set; }
+
+ ///
+ /// 消息
+ ///
+ public ChatMessage Message { get; set; } = new();
+
+ ///
+ /// 结束原因
+ ///
+ public string? FinishReason { get; set; }
+ }
+
+ ///
+ /// 流式响应
+ ///
+ public class ChatStreamResponse
+ {
+ public List? Choices { get; set; }
+ }
+
+ ///
+ /// 流式选择
+ ///
+ public class ChatStreamChoice
+ {
+ public ChatStreamDelta? Delta { get; set; }
+ }
+
+ ///
+ /// 流式增量
+ ///
+ public class ChatStreamDelta
+ {
+ public string? Content { get; set; }
+ }
+
+ ///
+ /// 使用情况
+ ///
+ public class UsageInfo
+ {
+ public int PromptTokens { get; set; }
+ public int CompletionTokens { get; set; }
+ public int TotalTokens { get; set; }
+ }
+
+ ///
+ /// 嵌入响应
+ ///
+ public class EmbeddingResponse
+ {
+ public List? Data { get; set; }
+ }
+
+ ///
+ /// 嵌入数据
+ ///
+ public class EmbeddingData
+ {
+ public float[]? Embedding { get; set; }
+ }
+
+ ///
+ /// 图像响应
+ ///
+ public class ImageResponse
+ {
+ public List? Data { get; set; }
+ }
+
+ ///
+ /// 图像数据
+ ///
+ public class ImageData
+ {
+ public string? Url { get; set; }
+ }
+
+ ///
+ /// 转录响应
+ ///
+ public class TranscriptionResponse
+ {
+ public string? Text { get; set; }
+ }
+
+ ///
+ /// OpenAI 异常
+ ///
+ public class OpenAIException : Exception
+ {
+ public string? ResponseJson { get; }
+
+ public OpenAIException(string message, string? responseJson = null) : base(message)
+ {
+ ResponseJson = responseJson;
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/EasyTool.Core/AICategory/PromptBuilder.cs b/EasyTool.Core/AICategory/PromptBuilder.cs
new file mode 100644
index 0000000..64743af
--- /dev/null
+++ b/EasyTool.Core/AICategory/PromptBuilder.cs
@@ -0,0 +1,315 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace EasyTool.AICategory
+{
+ ///
+ /// 提示词构建器
+ /// 提供构建和管理 AI 提示词的工具
+ ///
+ public class PromptBuilder
+ {
+ private readonly StringBuilder _systemPrompt = new();
+ private readonly List _examples = new();
+ private readonly List _context = new();
+ private readonly List _constraints = new();
+ private string? _task;
+ private string? _outputFormat;
+
+ ///
+ /// 设置系统提示词
+ ///
+ /// 系统提示词
+ /// 当前实例
+ public PromptBuilder SetSystemPrompt(string systemPrompt)
+ {
+ _systemPrompt.Clear();
+ _systemPrompt.Append(systemPrompt);
+ return this;
+ }
+
+ ///
+ /// 添加系统提示词
+ ///
+ /// 文本
+ /// 当前实例
+ public PromptBuilder AddSystemPrompt(string text)
+ {
+ _systemPrompt.AppendLine(text);
+ return this;
+ }
+
+ ///
+ /// 设置任务描述
+ ///
+ /// 任务描述
+ /// 当前实例
+ public PromptBuilder SetTask(string task)
+ {
+ _task = task;
+ return this;
+ }
+
+ ///
+ /// 添加示例
+ ///
+ /// 输入示例
+ /// 输出示例
+ /// 当前实例
+ public PromptBuilder AddExample(string input, string output)
+ {
+ _examples.Add($"输入: {input}\n输出: {output}");
+ return this;
+ }
+
+ ///
+ /// 添加上下文
+ ///
+ /// 上下文内容
+ /// 当前实例
+ public PromptBuilder AddContext(string context)
+ {
+ _context.Add(context);
+ return this;
+ }
+
+ ///
+ /// 添加约束条件
+ ///
+ /// 约束条件
+ /// 当前实例
+ public PromptBuilder AddConstraint(string constraint)
+ {
+ _constraints.Add(constraint);
+ return this;
+ }
+
+ ///
+ /// 设置输出格式
+ ///
+ /// 格式描述
+ /// 当前实例
+ public PromptBuilder SetOutputFormat(string format)
+ {
+ _outputFormat = format;
+ return this;
+ }
+
+ ///
+ /// 设置 JSON 输出格式
+ ///
+ /// JSON Schema 或示例
+ /// 当前实例
+ public PromptBuilder SetJsonOutput(string? schema = null)
+ {
+ if (!string.IsNullOrEmpty(schema))
+ {
+ _outputFormat = $"请以 JSON 格式输出,格式如下:\n{schema}";
+ }
+ else
+ {
+ _outputFormat = "请以有效的 JSON 格式输出,不要添加任何其他文本或解释。";
+ }
+ return this;
+ }
+
+ ///
+ /// 构建最终提示词
+ ///
+ /// 构建的提示词
+ public string Build()
+ {
+ var result = new StringBuilder();
+
+ // 系统提示词
+ if (_systemPrompt.Length > 0)
+ {
+ result.AppendLine(_systemPrompt.ToString());
+ result.AppendLine();
+ }
+
+ // 任务描述
+ if (!string.IsNullOrEmpty(_task))
+ {
+ result.AppendLine("## 任务");
+ result.AppendLine(_task);
+ result.AppendLine();
+ }
+
+ // 上下文
+ if (_context.Count > 0)
+ {
+ result.AppendLine("## 上下文");
+ foreach (var ctx in _context)
+ {
+ result.AppendLine(ctx);
+ }
+ result.AppendLine();
+ }
+
+ // 示例
+ if (_examples.Count > 0)
+ {
+ result.AppendLine("## 示例");
+ foreach (var example in _examples)
+ {
+ result.AppendLine(example);
+ result.AppendLine();
+ }
+ }
+
+ // 约束条件
+ if (_constraints.Count > 0)
+ {
+ result.AppendLine("## 约束条件");
+ foreach (var constraint in _constraints)
+ {
+ result.AppendLine($"- {constraint}");
+ }
+ result.AppendLine();
+ }
+
+ // 输出格式
+ if (!string.IsNullOrEmpty(_outputFormat))
+ {
+ result.AppendLine("## 输出格式");
+ result.AppendLine(_outputFormat);
+ }
+
+ return result.ToString().Trim();
+ }
+
+ ///
+ /// 构建消息列表
+ ///
+ /// 用户输入
+ /// 消息列表
+ public List BuildMessages(string userInput)
+ {
+ var messages = new List();
+
+ // 系统消息
+ var systemPrompt = Build();
+ if (!string.IsNullOrEmpty(systemPrompt))
+ {
+ messages.Add(new ChatMessage { Role = "system", Content = systemPrompt });
+ }
+
+ // 用户消息
+ messages.Add(new ChatMessage { Role = "user", Content = userInput });
+
+ return messages;
+ }
+
+ ///
+ /// 清空所有内容
+ ///
+ /// 当前实例
+ public PromptBuilder Clear()
+ {
+ _systemPrompt.Clear();
+ _examples.Clear();
+ _context.Clear();
+ _constraints.Clear();
+ _task = null;
+ _outputFormat = null;
+ return this;
+ }
+ }
+
+ ///
+ /// 常用提示词模板
+ ///
+ public static class PromptTemplates
+ {
+ ///
+ /// 代码审查提示词
+ ///
+ public static string CodeReview(string code, string? language = null)
+ {
+ var builder = new PromptBuilder()
+ .AddSystemPrompt("你是一位经验丰富的代码审查专家。")
+ .SetTask("审查以下代码并提供改进建议。")
+ .AddConstraint("关注代码质量、性能、安全性")
+ .AddConstraint("提供具体的改进建议")
+ .AddConstraint("指出潜在的问题和风险");
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ builder.AddContext($"编程语言: {language}");
+ }
+
+ builder.SetOutputFormat("请按以下格式输出:\n1. 问题列表\n2. 改进建议\n3. 重构后的代码(如有必要)");
+
+ var messages = builder.BuildMessages($"待审查的代码:\n```\n{code}\n```");
+ return messages[0].Content + "\n\n" + messages[1].Content;
+ }
+
+ ///
+ /// 翻译提示词
+ ///
+ public static string Translate(string text, string sourceLanguage, string targetLanguage)
+ {
+ var builder = new PromptBuilder()
+ .AddSystemPrompt("你是一位专业的翻译专家,精通多种语言。")
+ .SetTask($"将以下文本从{sourceLanguage}翻译成{targetLanguage}。")
+ .AddConstraint("保持原文的语气和风格")
+ .AddConstraint("确保翻译准确、自然、流畅")
+ .AddConstraint("保留专业术语的准确性");
+
+ var messages = builder.BuildMessages(text);
+ return messages[0].Content + "\n\n" + messages[1].Content;
+ }
+
+ ///
+ /// 摘要生成提示词
+ ///
+ public static string Summarize(string text, int? maxLength = null)
+ {
+ var builder = new PromptBuilder()
+ .AddSystemPrompt("你是一位专业的文本摘要专家。")
+ .SetTask("请为以下文本生成简洁的摘要。")
+ .AddConstraint("保留关键信息和要点")
+ .AddConstraint("语言简洁明了");
+
+ if (maxLength.HasValue)
+ {
+ builder.AddConstraint($"摘要长度不超过{maxLength.Value}字");
+ }
+
+ var messages = builder.BuildMessages(text);
+ return messages[0].Content + "\n\n" + messages[1].Content;
+ }
+
+ ///
+ /// 数据提取提示词
+ ///
+ public static string ExtractData(string text, string[] fields)
+ {
+ var builder = new PromptBuilder()
+ .AddSystemPrompt("你是一位数据提取专家。")
+ .SetTask("从以下文本中提取指定的数据字段。")
+ .SetJsonOutput($"{{\"fields\": [{string.Join(", ", fields)}]}}");
+
+ var messages = builder.BuildMessages(text);
+ return messages[0].Content + "\n\n" + messages[1].Content;
+ }
+
+ ///
+ /// 问答提示词
+ ///
+ public static string QuestionAnswer(string context, string question)
+ {
+ var builder = new PromptBuilder()
+ .AddSystemPrompt("你是一位知识渊博的助手,根据给定的上下文回答问题。")
+ .SetTask("根据提供的上下文回答用户的问题。")
+ .AddConstraint("只根据上下文内容回答")
+ .AddConstraint("如果上下文中没有相关信息,请明确说明")
+ .AddContext($"上下文:\n{context}");
+
+ var messages = builder.BuildMessages(question);
+ return messages[0].Content + "\n\n" + messages[1].Content;
+ }
+ }
+}
diff --git a/EasyTool.Core/AICategory/VectorSimilarity.cs b/EasyTool.Core/AICategory/VectorSimilarity.cs
new file mode 100644
index 0000000..b9eb150
--- /dev/null
+++ b/EasyTool.Core/AICategory/VectorSimilarity.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace EasyTool.AICategory
+{
+ ///
+ /// 向量相似度计算工具
+ /// 用于计算嵌入向量之间的相似度
+ ///
+ public static class VectorSimilarity
+ {
+ ///
+ /// 计算余弦相似度
+ ///
+ /// 向量1
+ /// 向量2
+ /// 相似度(-1到1)
+ public static double CosineSimilarity(float[] vector1, float[] vector2)
+ {
+ if (vector1.Length != vector2.Length)
+ throw new ArgumentException("向量长度必须相同");
+
+ double dotProduct = 0;
+ double magnitude1 = 0;
+ double magnitude2 = 0;
+
+ for (int i = 0; i < vector1.Length; i++)
+ {
+ dotProduct += vector1[i] * vector2[i];
+ magnitude1 += vector1[i] * vector1[i];
+ magnitude2 += vector2[i] * vector2[i];
+ }
+
+ magnitude1 = Math.Sqrt(magnitude1);
+ magnitude2 = Math.Sqrt(magnitude2);
+
+ if (magnitude1 == 0 || magnitude2 == 0)
+ return 0;
+
+ return dotProduct / (magnitude1 * magnitude2);
+ }
+
+ ///
+ /// 计算欧几里得距离
+ ///
+ /// 向量1
+ /// 向量2
+ /// 距离
+ public static double EuclideanDistance(float[] vector1, float[] vector2)
+ {
+ if (vector1.Length != vector2.Length)
+ throw new ArgumentException("向量长度必须相同");
+
+ double sum = 0;
+ for (int i = 0; i < vector1.Length; i++)
+ {
+ var diff = vector1[i] - vector2[i];
+ sum += diff * diff;
+ }
+
+ return Math.Sqrt(sum);
+ }
+
+ ///
+ /// 计算点积
+ ///
+ /// 向量1
+ /// 向量2
+ /// 点积
+ public static double DotProduct(float[] vector1, float[] vector2)
+ {
+ if (vector1.Length != vector2.Length)
+ throw new ArgumentException("向量长度必须相同");
+
+ double sum = 0;
+ for (int i = 0; i < vector1.Length; i++)
+ {
+ sum += vector1[i] * vector2[i];
+ }
+
+ return sum;
+ }
+
+ ///
+ /// 归一化向量
+ ///
+ /// 向量
+ /// 归一化后的向量
+ public static float[] Normalize(float[] vector)
+ {
+ double magnitude = 0;
+ for (int i = 0; i < vector.Length; i++)
+ {
+ magnitude += vector[i] * vector[i];
+ }
+
+ magnitude = Math.Sqrt(magnitude);
+ if (magnitude == 0)
+ return new float[vector.Length];
+
+ var result = new float[vector.Length];
+ for (int i = 0; i < vector.Length; i++)
+ {
+ result[i] = (float)(vector[i] / magnitude);
+ }
+
+ return result;
+ }
+
+ ///
+ /// 查找最相似的向量
+ ///
+ /// 查询向量
+ /// 候选向量列表
+ /// 返回数量
+ /// 最相似向量的索引和相似度
+ public static List<(int Index, double Similarity)> FindMostSimilar(float[] query, List candidates, int topK = 5)
+ {
+ var similarities = new List<(int Index, double Similarity)>();
+
+ for (int i = 0; i < candidates.Count; i++)
+ {
+ var similarity = CosineSimilarity(query, candidates[i]);
+ similarities.Add((i, similarity));
+ }
+
+ return similarities
+ .OrderByDescending(x => x.Similarity)
+ .Take(topK)
+ .ToList();
+ }
+ }
+
+ ///
+ /// 简单的向量存储
+ /// 用于存储和检索嵌入向量
+ ///
+ public class VectorStore
+ {
+ private readonly List _items = new();
+
+ ///
+ /// 添加向量
+ ///
+ /// ID
+ /// 向量
+ /// 元数据
+ public void Add(string id, float[] vector, Dictionary? metadata = null)
+ {
+ _items.Add(new VectorItem
+ {
+ Id = id,
+ Vector = vector,
+ Metadata = metadata ?? new Dictionary()
+ });
+ }
+
+ ///
+ /// 批量添加向量
+ ///
+ public void AddRange(IEnumerable<(string Id, float[] Vector, Dictionary? Metadata)> items)
+ {
+ foreach (var item in items)
+ {
+ Add(item.Id, item.Vector, item.Metadata);
+ }
+ }
+
+ ///
+ /// 搜索相似向量
+ ///
+ /// 查询向量
+ /// 返回数量
+ /// 最小相似度
+ /// 搜索结果
+ public List Search(float[] query, int topK = 5, double minScore = 0)
+ {
+ var results = new List();
+
+ foreach (var item in _items)
+ {
+ var score = VectorSimilarity.CosineSimilarity(query, item.Vector);
+ if (score >= minScore)
+ {
+ results.Add(new VectorSearchResult
+ {
+ Id = item.Id,
+ Score = score,
+ Metadata = item.Metadata
+ });
+ }
+ }
+
+ return results
+ .OrderByDescending(x => x.Score)
+ .Take(topK)
+ .ToList();
+ }
+
+ ///
+ /// 删除向量
+ ///
+ /// ID
+ /// 是否删除成功
+ public bool Remove(string id)
+ {
+ var item = _items.FirstOrDefault(x => x.Id == id);
+ if (item != null)
+ {
+ _items.Remove(item);
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// 清空所有向量
+ ///
+ public void Clear()
+ {
+ _items.Clear();
+ }
+
+ ///
+ /// 获取向量数量
+ ///
+ public int Count => _items.Count;
+
+ ///
+ /// 获取所有 ID
+ ///
+ public IEnumerable GetAllIds() => _items.Select(x => x.Id);
+ }
+
+ ///
+ /// 向量项
+ ///
+ internal class VectorItem
+ {
+ public string Id { get; set; } = string.Empty;
+ public float[] Vector { get; set; } = Array.Empty();
+ public Dictionary Metadata { get; set; } = new();
+ }
+
+ ///
+ /// 向量搜索结果
+ ///
+ public class VectorSearchResult
+ {
+ ///
+ /// ID
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// 相似度分数
+ ///
+ public double Score { get; set; }
+
+ ///
+ /// 元数据
+ ///
+ public Dictionary Metadata { get; set; } = new();
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/BankCardUtil.cs b/EasyTool.Core/BusinessCategory/BankCardUtil.cs
new file mode 100644
index 0000000..19e6c57
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/BankCardUtil.cs
@@ -0,0 +1,491 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 银行卡类型枚举
+ ///
+ public enum BankType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// 借记卡
+ ///
+ Debit = 1,
+
+ ///
+ /// 信用卡
+ ///
+ Credit = 2
+ }
+
+ ///
+ /// 银行信息
+ ///
+ public class BankInfo
+ {
+ ///
+ /// 银行名称
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 卡类型
+ ///
+ public BankType Type { get; set; }
+
+ ///
+ /// 银行缩写代码
+ ///
+ public string Code { get; set; } = string.Empty;
+ }
+
+ ///
+ /// 银行卡工具类
+ ///
+ public static class BankCardUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 银行卡号正则表达式(13-19位数字)
+ ///
+ private static readonly Regex BankCardRegex = new(@"^\d{13,19}$", RegexOptions.Compiled);
+
+ ///
+ /// 银行BIN码映射(前6位 -> 银行信息)
+ /// 注:此处仅包含部分常见银行BIN码,实际应用中应使用完整的BIN码库
+ ///
+ private static readonly Dictionary BinCodeMapping = new()
+ {
+ // 工商银行
+ { "622202", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622203", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622204", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622205", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622206", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622207", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622208", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622209", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622210", new BankInfo { Name = "中国工商银行", Type = BankType.Debit, Code = "ICBC" } },
+ { "622588", new BankInfo { Name = "中国工商银行", Type = BankType.Credit, Code = "ICBC" } },
+
+ // 农业银行
+ { "622848", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622849", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622845", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622846", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622847", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622821", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622822", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622823", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622824", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+ { "622825", new BankInfo { Name = "中国农业银行", Type = BankType.Debit, Code = "ABC" } },
+
+ // 中国银行
+ { "621660", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621661", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621662", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621663", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621665", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621666", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621667", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621668", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+ { "621669", new BankInfo { Name = "中国银行", Type = BankType.Debit, Code = "BOC" } },
+
+ // 建设银行
+ { "622700", new BankInfo { Name = "中国建设银行", Type = BankType.Debit, Code = "CCB" } },
+ { "622707", new BankInfo { Name = "中国建设银行", Type = BankType.Debit, Code = "CCB" } },
+ { "622708", new BankInfo { Name = "中国建设银行", Type = BankType.Debit, Code = "CCB" } },
+ { "621081", new BankInfo { Name = "中国建设银行", Type = BankType.Debit, Code = "CCB" } },
+ { "436742", new BankInfo { Name = "中国建设银行", Type = BankType.Credit, Code = "CCB" } },
+ { "436745", new BankInfo { Name = "中国建设银行", Type = BankType.Credit, Code = "CCB" } },
+
+ // 交通银行
+ { "622260", new BankInfo { Name = "交通银行", Type = BankType.Debit, Code = "BOCOM" } },
+ { "622261", new BankInfo { Name = "交通银行", Type = BankType.Debit, Code = "BOCOM" } },
+ { "622262", new BankInfo { Name = "交通银行", Type = BankType.Debit, Code = "BOCOM" } },
+ { "622521", new BankInfo { Name = "交通银行", Type = BankType.Credit, Code = "BOCOM" } },
+ { "622522", new BankInfo { Name = "交通银行", Type = BankType.Credit, Code = "BOCOM" } },
+
+ // 招商银行
+ { "622580", new BankInfo { Name = "招商银行", Type = BankType.Debit, Code = "CMB" } },
+ { "622581", new BankInfo { Name = "招商银行", Type = BankType.Debit, Code = "CMB" } },
+ { "622582", new BankInfo { Name = "招商银行", Type = BankType.Debit, Code = "CMB" } },
+ { "622588", new BankInfo { Name = "招商银行", Type = BankType.Credit, Code = "CMB" } },
+ { "622589", new BankInfo { Name = "招商银行", Type = BankType.Credit, Code = "CMB" } },
+ { "621286", new BankInfo { Name = "招商银行", Type = BankType.Debit, Code = "CMB" } },
+
+ // 浦发银行
+ { "622518", new BankInfo { Name = "浦发银行", Type = BankType.Debit, Code = "SPDB" } },
+ { "622519", new BankInfo { Name = "浦发银行", Type = BankType.Debit, Code = "SPDB" } },
+ { "622520", new BankInfo { Name = "浦发银行", Type = BankType.Credit, Code = "SPDB" } },
+ { "621228", new BankInfo { Name = "浦发银行", Type = BankType.Debit, Code = "SPDB" } },
+
+ // 民生银行
+ { "622615", new BankInfo { Name = "民生银行", Type = BankType.Debit, Code = "CMBC" } },
+ { "622617", new BankInfo { Name = "民生银行", Type = BankType.Debit, Code = "CMBC" } },
+ { "622618", new BankInfo { Name = "民生银行", Type = BankType.Debit, Code = "CMBC" } },
+ { "622620", new BankInfo { Name = "民生银行", Type = BankType.Credit, Code = "CMBC" } },
+ { "622622", new BankInfo { Name = "民生银行", Type = BankType.Credit, Code = "CMBC" } },
+
+ // 兴业银行
+ { "622909", new BankInfo { Name = "兴业银行", Type = BankType.Debit, Code = "CIB" } },
+ { "622910", new BankInfo { Name = "兴业银行", Type = BankType.Debit, Code = "CIB" } },
+ { "622911", new BankInfo { Name = "兴业银行", Type = BankType.Debit, Code = "CIB" } },
+ { "622912", new BankInfo { Name = "兴业银行", Type = BankType.Debit, Code = "CIB" } },
+ { "622918", new BankInfo { Name = "兴业银行", Type = BankType.Credit, Code = "CIB" } },
+
+ // 中信银行
+ { "622690", new BankInfo { Name = "中信银行", Type = BankType.Debit, Code = "CITIC" } },
+ { "622691", new BankInfo { Name = "中信银行", Type = BankType.Debit, Code = "CITIC" } },
+ { "622692", new BankInfo { Name = "中信银行", Type = BankType.Debit, Code = "CITIC" } },
+ { "622696", new BankInfo { Name = "中信银行", Type = BankType.Credit, Code = "CITIC" } },
+ { "622698", new BankInfo { Name = "中信银行", Type = BankType.Credit, Code = "CITIC" } },
+
+ // 光大银行
+ { "622655", new BankInfo { Name = "光大银行", Type = BankType.Debit, Code = "CEB" } },
+ { "622656", new BankInfo { Name = "光大银行", Type = BankType.Debit, Code = "CEB" } },
+ { "622657", new BankInfo { Name = "光大银行", Type = BankType.Debit, Code = "CEB" } },
+ { "622658", new BankInfo { Name = "光大银行", Type = BankType.Credit, Code = "CEB" } },
+ { "622685", new BankInfo { Name = "光大银行", Type = BankType.Credit, Code = "CEB" } },
+
+ // 平安银行
+ { "622155", new BankInfo { Name = "平安银行", Type = BankType.Debit, Code = "PAB" } },
+ { "622156", new BankInfo { Name = "平安银行", Type = BankType.Debit, Code = "PAB" } },
+ { "622157", new BankInfo { Name = "平安银行", Type = BankType.Debit, Code = "PAB" } },
+ { "622525", new BankInfo { Name = "平安银行", Type = BankType.Credit, Code = "PAB" } },
+ { "622526", new BankInfo { Name = "平安银行", Type = BankType.Credit, Code = "PAB" } },
+
+ // 华夏银行
+ { "622630", new BankInfo { Name = "华夏银行", Type = BankType.Debit, Code = "HXB" } },
+ { "622631", new BankInfo { Name = "华夏银行", Type = BankType.Debit, Code = "HXB" } },
+ { "622632", new BankInfo { Name = "华夏银行", Type = BankType.Debit, Code = "HXB" } },
+
+ // 广发银行
+ { "622568", new BankInfo { Name = "广发银行", Type = BankType.Debit, Code = "CGB" } },
+ { "622569", new BankInfo { Name = "广发银行", Type = BankType.Debit, Code = "CGB" } },
+ { "622570", new BankInfo { Name = "广发银行", Type = BankType.Credit, Code = "CGB" } },
+ { "622575", new BankInfo { Name = "广发银行", Type = BankType.Credit, Code = "CGB" } },
+
+ // 邮储银行
+ { "622150", new BankInfo { Name = "邮储银行", Type = BankType.Debit, Code = "PSBC" } },
+ { "622151", new BankInfo { Name = "邮储银行", Type = BankType.Debit, Code = "PSBC" } },
+ { "622180", new BankInfo { Name = "邮储银行", Type = BankType.Debit, Code = "PSBC" } },
+ { "622181", new BankInfo { Name = "邮储银行", Type = BankType.Debit, Code = "PSBC" } },
+ { "622188", new BankInfo { Name = "邮储银行", Type = BankType.Debit, Code = "PSBC" } },
+
+ // 北京银行
+ { "622309", new BankInfo { Name = "北京银行", Type = BankType.Debit, Code = "BJBANK" } },
+ { "622310", new BankInfo { Name = "北京银行", Type = BankType.Debit, Code = "BJBANK" } },
+ { "622311", new BankInfo { Name = "北京银行", Type = BankType.Debit, Code = "BJBANK" } },
+
+ // 上海银行
+ { "622462", new BankInfo { Name = "上海银行", Type = BankType.Debit, Code = "SHBANK" } },
+ { "622463", new BankInfo { Name = "上海银行", Type = BankType.Debit, Code = "SHBANK" } },
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证银行卡号是否有效(格式 + Luhn校验)
+ ///
+ /// 银行卡号
+ /// 是否有效
+ public static bool IsValid(string? cardNumber)
+ {
+ if (!IsValidFormat(cardNumber))
+ {
+ return false;
+ }
+
+ return ValidateLuhn(cardNumber);
+ }
+
+ ///
+ /// 仅验证银行卡号格式(不包含Luhn校验)
+ ///
+ /// 银行卡号
+ /// 格式是否有效
+ public static bool IsValidFormat(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber))
+ {
+ return false;
+ }
+
+ return BankCardRegex.IsMatch(cardNumber);
+ }
+
+ ///
+ /// 使用Luhn算法验证银行卡号
+ ///
+ /// 银行卡号
+ /// 是否通过Luhn校验
+ public static bool ValidateLuhn(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber))
+ {
+ return false;
+ }
+
+ int sum = 0;
+ int length = cardNumber.Length;
+ bool isEvenPosition = false;
+
+ // 从右向左遍历
+ for (int i = length - 1; i >= 0; i--)
+ {
+ if (!char.IsDigit(cardNumber[i]))
+ {
+ return false;
+ }
+
+ int digit = cardNumber[i] - '0';
+
+ if (isEvenPosition)
+ {
+ digit *= 2;
+ if (digit > 9)
+ {
+ digit -= 9;
+ }
+ }
+
+ sum += digit;
+ isEvenPosition = !isEvenPosition;
+ }
+
+ return sum % 10 == 0;
+ }
+
+ ///
+ /// 计算Luhn校验位
+ ///
+ /// 不含校验位的银行卡号
+ /// 校验位(0-9),计算失败返回-1
+ public static int CalculateLuhnCheckDigit(string? cardNumberWithoutCheckDigit)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumberWithoutCheckDigit))
+ {
+ return -1;
+ }
+
+ // 在末尾添加一个临时校验位0
+ string tempCardNumber = cardNumberWithoutCheckDigit + "0";
+ int sum = 0;
+ int length = tempCardNumber.Length;
+ bool isEvenPosition = false;
+
+ for (int i = length - 1; i >= 0; i--)
+ {
+ if (!char.IsDigit(tempCardNumber[i]))
+ {
+ return -1;
+ }
+
+ int digit = tempCardNumber[i] - '0';
+
+ if (isEvenPosition)
+ {
+ digit *= 2;
+ if (digit > 9)
+ {
+ digit -= 9;
+ }
+ }
+
+ sum += digit;
+ isEvenPosition = !isEvenPosition;
+ }
+
+ return (10 - (sum % 10)) % 10;
+ }
+
+ #endregion
+
+ #region 银行信息查询
+
+ ///
+ /// 获取银行信息
+ ///
+ /// 银行卡号
+ /// 银行信息,未找到返回null
+ public static BankInfo? GetBankInfo(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber) || cardNumber.Length < 6)
+ {
+ return null;
+ }
+
+ // 尝试匹配6位BIN码
+ string bin6 = cardNumber.Substring(0, 6);
+ if (BinCodeMapping.TryGetValue(bin6, out BankInfo? info))
+ {
+ return info;
+ }
+
+ // 尝试匹配5位BIN码
+ if (cardNumber.Length >= 5)
+ {
+ string bin5 = cardNumber.Substring(0, 5);
+ if (BinCodeMapping.TryGetValue(bin5, out info))
+ {
+ return info;
+ }
+ }
+
+ // 尝试匹配4位BIN码
+ if (cardNumber.Length >= 4)
+ {
+ string bin4 = cardNumber.Substring(0, 4);
+ if (BinCodeMapping.TryGetValue(bin4, out info))
+ {
+ return info;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取银行名称
+ ///
+ /// 银行卡号
+ /// 银行名称
+ public static string? GetBankName(string? cardNumber)
+ {
+ return GetBankInfo(cardNumber)?.Name;
+ }
+
+ ///
+ /// 获取银行缩写代码
+ ///
+ /// 银行卡号
+ /// 银行缩写代码
+ public static string? GetBankCode(string? cardNumber)
+ {
+ return GetBankInfo(cardNumber)?.Code;
+ }
+
+ ///
+ /// 获取卡类型
+ ///
+ /// 银行卡号
+ /// 卡类型
+ public static BankType GetBankType(string? cardNumber)
+ {
+ return GetBankInfo(cardNumber)?.Type ?? BankType.Unknown;
+ }
+
+ ///
+ /// 判断是否为借记卡
+ ///
+ /// 银行卡号
+ /// 是否为借记卡
+ public static bool IsDebitCard(string? cardNumber)
+ {
+ return GetBankType(cardNumber) == BankType.Debit;
+ }
+
+ ///
+ /// 判断是否为信用卡
+ ///
+ /// 银行卡号
+ /// 是否为信用卡
+ public static bool IsCreditCard(string? cardNumber)
+ {
+ return GetBankType(cardNumber) == BankType.Credit;
+ }
+
+ ///
+ /// 获取BIN码(前6位)
+ ///
+ /// 银行卡号
+ /// BIN码
+ public static string? GetBinCode(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber) || cardNumber.Length < 6)
+ {
+ return null;
+ }
+
+ return cardNumber.Substring(0, 6);
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化银行卡号(每4位一组,空格分隔)
+ ///
+ /// 银行卡号
+ /// 格式化后的卡号
+ public static string? Format(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber))
+ {
+ return null;
+ }
+
+ // 移除非数字字符
+ string cleaned = Regex.Replace(cardNumber, @"\D", "");
+
+ if (!IsValidFormat(cleaned))
+ {
+ return null;
+ }
+
+ // 每4位分组
+ var groups = new List();
+ for (int i = 0; i < cleaned.Length; i += 4)
+ {
+ int length = Math.Min(4, cleaned.Length - i);
+ groups.Add(cleaned.Substring(i, length));
+ }
+
+ return string.Join(" ", groups);
+ }
+
+ ///
+ /// 银行卡号脱敏:6222****5678
+ ///
+ /// 银行卡号
+ /// 脱敏后的卡号
+ public static string? Mask(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber))
+ {
+ return null;
+ }
+
+ // 移除非数字字符
+ string cleaned = Regex.Replace(cardNumber, @"\D", "");
+
+ if (cleaned.Length < 8)
+ {
+ return null;
+ }
+
+ int prefixLength = 4;
+ int suffixLength = 4;
+
+ string prefix = cleaned.Substring(0, prefixLength);
+ string suffix = cleaned.Substring(cleaned.Length - suffixLength, suffixLength);
+ int maskLength = cleaned.Length - prefixLength - suffixLength;
+
+ return prefix + new string('*', maskLength) + suffix;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/BarcodeUtil.cs b/EasyTool.Core/BusinessCategory/BarcodeUtil.cs
new file mode 100644
index 0000000..e541ba5
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/BarcodeUtil.cs
@@ -0,0 +1,651 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 条形码类型枚举
+ ///
+ public enum BarcodeType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// EAN-13(13位国际商品条码)
+ ///
+ EAN13 = 1,
+
+ ///
+ /// EAN-8(8位商品条码)
+ ///
+ EAN8 = 2,
+
+ ///
+ /// UPC-A(12位北美商品条码)
+ ///
+ UPCA = 3,
+
+ ///
+ /// UPC-E(6位压缩商品条码)
+ ///
+ UPCE = 4,
+
+ ///
+ /// ITF-14(14位物流包装条码)
+ ///
+ ITF14 = 5,
+
+ ///
+ /// Code128(可变长度工业条码)
+ ///
+ Code128 = 6
+ }
+
+ ///
+ /// 条形码工具类
+ ///
+ public static class BarcodeUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// EAN-13正则表达式
+ ///
+ private static readonly Regex EAN13Regex = new(@"^\d{13}$", RegexOptions.Compiled);
+
+ ///
+ /// EAN-8正则表达式
+ ///
+ private static readonly Regex EAN8Regex = new(@"^\d{8}$", RegexOptions.Compiled);
+
+ ///
+ /// UPC-A正则表达式
+ ///
+ private static readonly Regex UPCARegex = new(@"^\d{12}$", RegexOptions.Compiled);
+
+ ///
+ /// UPC-E正则表达式
+ ///
+ private static readonly Regex UPCERegex = new(@"^\d{6}$", RegexOptions.Compiled);
+
+ ///
+ /// ITF-14正则表达式
+ ///
+ private static readonly Regex ITF14Regex = new(@"^\d{14}$", RegexOptions.Compiled);
+
+ ///
+ /// 国家代码(GS1前缀)与地区映射
+ ///
+ private static readonly (string Prefix, string Region)[] Gs1PrefixMap =
+ {
+ ("000", "美国/加拿大"), ("001", "美国/加拿大"), ("019", "美国/加拿大"),
+ ("020", "店内码"), ("029", "店内码"),
+ ("030", "美国/加拿大"), ("039", "美国/加拿大"),
+ ("040", "店内码"), ("049", "店内码"),
+ ("050", "优惠券"), ("099", "优惠券"),
+ ("100", "美国/加拿大"), ("139", "美国/加拿大"),
+ ("200", "店内码"), ("299", "店内码"),
+ ("300", "法国"), ("379", "法国"),
+ ("380", "保加利亚"),
+ ("383", "斯洛文尼亚"),
+ ("385", "克罗地亚"),
+ ("387", "波黑"),
+ ("400", "德国"), ("440", "德国"),
+ ("450", "日本"), ("459", "日本"),
+ ("460", "俄罗斯"), ("469", "俄罗斯"),
+ ("470", "吉尔吉斯斯坦"),
+ ("471", "台湾"),
+ ("474", "爱沙尼亚"),
+ ("475", "拉脱维亚"),
+ ("476", "阿塞拜疆"),
+ ("477", "立陶宛"),
+ ("478", "乌兹别克斯坦"),
+ ("479", "斯里兰卡"),
+ ("480", "菲律宾"),
+ ("481", "白俄罗斯"),
+ ("482", "乌克兰"),
+ ("483", "土库曼斯坦"),
+ ("484", "摩尔多瓦"),
+ ("485", "亚美尼亚"),
+ ("486", "格鲁吉亚"),
+ ("487", "哈萨克斯坦"),
+ ("488", "塔吉克斯坦"),
+ ("489", "香港"),
+ ("490", "日本"), ("499", "日本"),
+ ("500", "英国"), ("509", "英国"),
+ ("520", "希腊"),
+ ("528", "黎巴嫩"),
+ ("529", "塞浦路斯"),
+ ("530", "阿尔巴尼亚"),
+ ("531", "马其顿"),
+ ("535", "马耳他"),
+ ("539", "爱尔兰"),
+ ("540", "比利时/卢森堡"), ("549", "比利时/卢森堡"),
+ ("560", "葡萄牙"),
+ ("569", "冰岛"),
+ ("570", "丹麦"), ("579", "丹麦"),
+ ("590", "波兰"),
+ ("594", "罗马尼亚"),
+ ("599", "匈牙利"),
+ ("600", "南非"), ("601", "南非"),
+ ("603", "加纳"),
+ ("604", "塞内加尔"),
+ ("608", "巴林"),
+ ("609", "毛里求斯"),
+ ("611", "摩洛哥"),
+ ("613", "阿尔及利亚"),
+ ("615", "尼日利亚"),
+ ("616", "肯尼亚"),
+ ("618", "科特迪瓦"),
+ ("619", "突尼斯"),
+ ("621", "叙利亚"),
+ ("622", "埃及"),
+ ("624", "利比亚"),
+ ("625", "约旦"),
+ ("626", "伊朗"),
+ ("627", "科威特"),
+ ("628", "沙特阿拉伯"),
+ ("629", "阿联酋"),
+ ("640", "芬兰"), ("649", "芬兰"),
+ ("690", "中国"), ("699", "中国"),
+ ("700", "挪威"), ("709", "挪威"),
+ ("729", "以色列"),
+ ("730", "瑞典"), ("739", "瑞典"),
+ ("740", "危地马拉"),
+ ("741", "萨尔瓦多"),
+ ("742", "洪都拉斯"),
+ ("743", "尼加拉瓜"),
+ ("744", "哥斯达黎加"),
+ ("745", "巴拿马"),
+ ("746", "多米尼加"),
+ ("750", "墨西哥"),
+ ("754", "加拿大"), ("755", "加拿大"),
+ ("759", "委内瑞拉"),
+ ("760", "瑞士"), ("769", "瑞士"),
+ ("770", "哥伦比亚"),
+ ("773", "乌拉圭"),
+ ("775", "秘鲁"),
+ ("777", "玻利维亚"),
+ ("779", "阿根廷"),
+ ("780", "智利"),
+ ("784", "巴拉圭"),
+ ("786", "厄瓜多尔"),
+ ("789", "巴西"), ("790", "巴西"),
+ ("800", "意大利"), ("839", "意大利"),
+ ("840", "美国"), ("849", "美国"),
+ ("850", "古巴"),
+ ("858", "斯洛伐克"),
+ ("859", "捷克"),
+ ("860", "塞尔维亚"),
+ ("865", "蒙古"),
+ ("867", "朝鲜"),
+ ("868", "土耳其"), ("869", "土耳其"),
+ ("870", "荷兰"), ("879", "荷兰"),
+ ("880", "韩国"),
+ ("884", "柬埔寨"),
+ ("885", "泰国"),
+ ("888", "新加坡"),
+ ("890", "印度"),
+ ("893", "越南"),
+ ("896", "巴基斯坦"),
+ ("899", "印度尼西亚"),
+ ("900", "奥地利"), ("919", "奥地利"),
+ ("930", "澳大利亚"), ("939", "澳大利亚"),
+ ("940", "新西兰"), ("949", "新西兰"),
+ ("950", "国际组织"),
+ ("951", "国际组织"),
+ ("955", "马来西亚"),
+ ("958", "澳门")
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证条形码是否有效(自动识别类型)
+ ///
+ /// 条形码
+ /// 是否有效
+ public static bool IsValid(string? barcode)
+ {
+ return IsValidEAN13(barcode) || IsValidEAN8(barcode) ||
+ IsValidUPCA(barcode) || IsValidUPCE(barcode) ||
+ IsValidITF14(barcode);
+ }
+
+ ///
+ /// 验证EAN-13条形码是否有效
+ ///
+ /// 条形码
+ /// 是否有效
+ public static bool IsValidEAN13(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode) || !EAN13Regex.IsMatch(barcode))
+ {
+ return false;
+ }
+
+ return ValidateChecksum(barcode, 13);
+ }
+
+ ///
+ /// 验证EAN-8条形码是否有效
+ ///
+ /// 条形码
+ /// 是否有效
+ public static bool IsValidEAN8(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode) || !EAN8Regex.IsMatch(barcode))
+ {
+ return false;
+ }
+
+ return ValidateChecksum(barcode, 8);
+ }
+
+ ///
+ /// 验证UPC-A条形码是否有效
+ ///
+ /// 条形码
+ /// 是否有效
+ public static bool IsValidUPCA(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode) || !UPCARegex.IsMatch(barcode))
+ {
+ return false;
+ }
+
+ return ValidateChecksum(barcode, 12);
+ }
+
+ ///
+ /// 验证UPC-E条形码是否有效
+ ///
+ /// 条形码
+ /// 是否有效
+ public static bool IsValidUPCE(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode) || !UPCERegex.IsMatch(barcode))
+ {
+ return false;
+ }
+
+ // UPC-E需要展开为UPC-A后验证
+ string? expanded = ExpandUPCE(barcode);
+ return expanded != null && IsValidUPCA(expanded);
+ }
+
+ ///
+ /// 验证ITF-14条形码是否有效
+ ///
+ /// 条形码
+ /// 是否有效
+ public static bool IsValidITF14(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode) || !ITF14Regex.IsMatch(barcode))
+ {
+ return false;
+ }
+
+ return ValidateChecksum(barcode, 14);
+ }
+
+ ///
+ /// 验证校验位
+ ///
+ private static bool ValidateChecksum(string barcode, int length)
+ {
+ int sum = 0;
+ for (int i = 0; i < length - 1; i++)
+ {
+ int digit = barcode[i] - '0';
+ // 从右向左,偶数位权重为3,奇数位权重为1
+ int weight = ((length - 1 - i) % 2 == 1) ? 3 : 1;
+ sum += digit * weight;
+ }
+
+ int checkDigit = (10 - (sum % 10)) % 10;
+ return checkDigit == (barcode[length - 1] - '0');
+ }
+
+ ///
+ /// 计算校验位
+ ///
+ /// 不含校验位的条形码
+ /// 校验位(0-9),计算失败返回-1
+ public static int CalculateCheckDigit(string? barcodeWithoutCheck)
+ {
+ if (string.IsNullOrWhiteSpace(barcodeWithoutCheck))
+ {
+ return -1;
+ }
+
+ int length = barcodeWithoutCheck.Length;
+ int sum = 0;
+ for (int i = 0; i < length; i++)
+ {
+ if (!char.IsDigit(barcodeWithoutCheck[i]))
+ {
+ return -1;
+ }
+ int digit = barcodeWithoutCheck[i] - '0';
+ // 从右向左,偶数位权重为3
+ int weight = ((length - i) % 2 == 0) ? 3 : 1;
+ sum += digit * weight;
+ }
+
+ return (10 - (sum % 10)) % 10;
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取条形码类型
+ ///
+ /// 条形码
+ /// 条形码类型
+ public static BarcodeType GetBarcodeType(string? barcode)
+ {
+ if (IsValidEAN13(barcode)) return BarcodeType.EAN13;
+ if (IsValidEAN8(barcode)) return BarcodeType.EAN8;
+ if (IsValidUPCA(barcode)) return BarcodeType.UPCA;
+ if (IsValidUPCE(barcode)) return BarcodeType.UPCE;
+ if (IsValidITF14(barcode)) return BarcodeType.ITF14;
+ return BarcodeType.Unknown;
+ }
+
+ ///
+ /// 获取条形码类型名称
+ ///
+ /// 条形码类型
+ /// 类型名称
+ public static string GetBarcodeTypeName(BarcodeType type)
+ {
+ return type switch
+ {
+ BarcodeType.EAN13 => "EAN-13",
+ BarcodeType.EAN8 => "EAN-8",
+ BarcodeType.UPCA => "UPC-A",
+ BarcodeType.UPCE => "UPC-E",
+ BarcodeType.ITF14 => "ITF-14",
+ BarcodeType.Code128 => "Code 128",
+ _ => "未知"
+ };
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取国家/地区(根据GS1前缀)
+ ///
+ /// 条形码
+ /// 国家/地区名称
+ public static string? GetRegion(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode) || barcode.Length < 3)
+ {
+ return null;
+ }
+
+ string prefix3 = barcode.Substring(0, 3);
+ string prefix2 = barcode.Substring(0, 2);
+ string prefix1 = barcode.Substring(0, 1);
+
+ // 先匹配3位前缀
+ foreach (var mapping in Gs1PrefixMap)
+ {
+ if (mapping.Prefix == prefix3)
+ {
+ return mapping.Region;
+ }
+ }
+
+ // 再匹配2位前缀
+ foreach (var mapping in Gs1PrefixMap)
+ {
+ if (mapping.Prefix == prefix2)
+ {
+ return mapping.Region;
+ }
+ }
+
+ // 最后匹配1位前缀
+ foreach (var mapping in Gs1PrefixMap)
+ {
+ if (mapping.Prefix == prefix1)
+ {
+ return mapping.Region;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 判断是否为中国商品条码
+ ///
+ /// 条形码
+ /// 是否为中国商品条码
+ public static bool IsChinaBarcode(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode) || barcode.Length < 3)
+ {
+ return false;
+ }
+
+ string prefix = barcode.Substring(0, 3);
+ return prefix.CompareTo("690") >= 0 && prefix.CompareTo("699") <= 0;
+ }
+
+ ///
+ /// 获取厂商识别代码(EAN-13的前7-9位)
+ ///
+ /// 条形码
+ /// 厂商识别代码
+ public static string? GetManufacturerCode(string? barcode)
+ {
+ if (!IsValidEAN13(barcode))
+ {
+ return null;
+ }
+
+ // EAN-13:前缀(2-3位) + 厂商代码(4-5位) + 商品代码(5位) + 校验位
+ // 简化处理:返回前8位(不含校验位)
+ return barcode!.Substring(0, 8);
+ }
+
+ ///
+ /// 获取商品项目代码(EAN-13的第9-12位)
+ ///
+ /// 条形码
+ /// 商品项目代码
+ public static string? GetProductCode(string? barcode)
+ {
+ if (!IsValidEAN13(barcode))
+ {
+ return null;
+ }
+
+ return barcode!.Substring(8, 4);
+ }
+
+ #endregion
+
+ #region 转换方法
+
+ ///
+ /// 将UPC-E转换为UPC-A
+ ///
+ /// UPC-E条形码
+ /// UPC-A条形码,转换失败返回null
+ public static string? ExpandUPCE(string? upce)
+ {
+ if (string.IsNullOrWhiteSpace(upce) || upce.Length != 6 || !UPCERegex.IsMatch(upce))
+ {
+ return null;
+ }
+
+ char lastDigit = upce[5];
+ string result;
+
+ switch (lastDigit)
+ {
+ case '0':
+ result = upce[0] + upce[1].ToString() + "00000" + upce[2] + upce[3] + upce[4];
+ break;
+ case '1':
+ result = upce[0] + upce[1].ToString() + "10000" + upce[2] + upce[3] + upce[4];
+ break;
+ case '2':
+ result = upce[0] + upce[1].ToString() + "20000" + upce[2] + upce[3] + upce[4];
+ break;
+ case '3':
+ result = upce[0] + upce[1].ToString() + upce[2] + "00000" + upce[3] + upce[4];
+ break;
+ case '4':
+ result = upce[0] + upce[1].ToString() + upce[2] + upce[3] + "00000" + upce[4];
+ break;
+ default:
+ result = upce[0] + upce[1].ToString() + upce[2] + upce[3] + upce[4] + "0000" + lastDigit;
+ break;
+ }
+
+ // 添加系统字符(0)和计算校验位
+ string fullCode = "0" + result;
+ int checkDigit = CalculateCheckDigit(fullCode);
+ return checkDigit >= 0 ? fullCode + checkDigit : null;
+ }
+
+ ///
+ /// 将UPC-A转换为EAN-13
+ ///
+ /// UPC-A条形码
+ /// EAN-13条形码
+ public static string? ConvertUPCAToEAN13(string? upca)
+ {
+ if (!IsValidUPCA(upca))
+ {
+ return null;
+ }
+
+ return "0" + upca;
+ }
+
+ ///
+ /// 将EAN-13转换为EAN-8(仅当适用于短码时)
+ ///
+ /// EAN-13条形码
+ /// EAN-8条形码,不适用返回null
+ public static string? ConvertEAN13ToEAN8(string? ean13)
+ {
+ if (!IsValidEAN13(ean13))
+ {
+ return null;
+ }
+
+ // 只有特定前缀的EAN-13才能转换为EAN-8
+ // 简化处理:仅支持部分转换
+ return null;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化条形码(去除非数字字符)
+ ///
+ /// 条形码
+ /// 格式化后的条形码
+ public static string? Normalize(string? barcode)
+ {
+ if (string.IsNullOrWhiteSpace(barcode))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(barcode, @"\D", "");
+ return cleaned.Length >= 6 ? cleaned : null;
+ }
+
+ ///
+ /// 格式化EAN-13(X-XXXXXX-XXXXX-X)
+ ///
+ /// 条形码
+ /// 格式化后的条形码
+ public static string? FormatEAN13(string? barcode)
+ {
+ if (!IsValidEAN13(barcode))
+ {
+ return null;
+ }
+
+ return $"{barcode![0]}-{barcode.Substring(1, 6)}-{barcode.Substring(7, 5)}-{barcode[12]}";
+ }
+
+ ///
+ /// 条形码脱敏:69****1234
+ ///
+ /// 条形码
+ /// 脱敏后的条形码
+ public static string? Mask(string? barcode)
+ {
+ string? normalized = Normalize(barcode);
+ if (normalized == null || normalized.Length < 6)
+ {
+ return null;
+ }
+
+ int len = normalized.Length;
+ int prefixLen = Math.Min(2, len / 3);
+ int suffixLen = Math.Min(4, len / 3);
+
+ return normalized.Substring(0, prefixLen) +
+ new string('*', len - prefixLen - suffixLen) +
+ normalized.Substring(len - suffixLen);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机EAN-13条形码(仅供测试使用)
+ ///
+ /// 前缀(可选,默认690-中国)
+ /// EAN-13条形码
+ public static string GenerateRandomEAN13(string? prefix = null)
+ {
+ string pre = prefix ?? "690";
+ while (pre.Length < 12)
+ {
+ pre += MathCategory.RandomUtil.RandomInt(0, 10).ToString();
+ }
+
+ pre = pre.Substring(0, 12);
+ int checkDigit = CalculateCheckDigit(pre);
+ return pre + checkDigit;
+ }
+
+ ///
+ /// 生成随机ITF-14条形码(仅供测试使用)
+ ///
+ /// ITF-14条形码
+ public static string GenerateRandomITF14()
+ {
+ string code = MathCategory.RandomUtil.RandomDigitString(13);
+ int checkDigit = CalculateCheckDigit(code);
+ return code + checkDigit;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/CreditCardUtil.cs b/EasyTool.Core/BusinessCategory/CreditCardUtil.cs
new file mode 100644
index 0000000..47cf073
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/CreditCardUtil.cs
@@ -0,0 +1,484 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 国际信用卡类型枚举
+ ///
+ public enum CreditCardType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// Visa
+ ///
+ Visa = 1,
+
+ ///
+ /// MasterCard
+ ///
+ MasterCard = 2,
+
+ ///
+ /// American Express
+ ///
+ Amex = 3,
+
+ ///
+ /// Discover
+ ///
+ Discover = 4,
+
+ ///
+ /// JCB
+ ///
+ JCB = 5,
+
+ ///
+ /// Diners Club
+ ///
+ DinersClub = 6,
+
+ ///
+ /// UnionPay(银联)
+ ///
+ UnionPay = 7,
+
+ ///
+ /// Maestro
+ ///
+ Maestro = 8
+ }
+
+ ///
+ /// 国际信用卡信息
+ ///
+ public class CreditCardInfo
+ {
+ ///
+ /// 卡类型
+ ///
+ public CreditCardType Type { get; set; }
+
+ ///
+ /// 卡名称
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 发卡组织
+ ///
+ public string Issuer { get; set; } = string.Empty;
+ }
+
+ ///
+ /// 国际信用卡工具类
+ ///
+ public static class CreditCardUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 信用卡号正则表达式(13-19位数字)
+ ///
+ private static readonly Regex CardNumberRegex = new(@"^\d{13,19}$", RegexOptions.Compiled);
+
+ ///
+ /// 卡类型识别规则(前缀 -> 卡类型)
+ ///
+ private static readonly (string Prefix, CreditCardType Type, string Name, string Issuer)[] CardTypeRules =
+ {
+ // Visa: 4开头,13或16位
+ ("4", CreditCardType.Visa, "Visa", "Visa International"),
+
+ // MasterCard: 51-55, 2221-2720开头,16位
+ ("51", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("52", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("53", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("54", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("55", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2221", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2222", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2223", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2224", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2225", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2226", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2227", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2228", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2229", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("223", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("224", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("225", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("226", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("227", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("228", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("229", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("23", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("24", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("25", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("26", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("270", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("271", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+ ("2720", CreditCardType.MasterCard, "MasterCard", "MasterCard Worldwide"),
+
+ // American Express: 34或37开头,15位
+ ("34", CreditCardType.Amex, "American Express", "American Express Company"),
+ ("37", CreditCardType.Amex, "American Express", "American Express Company"),
+
+ // Discover: 6011, 622126-622925, 644-649, 65开头,16位
+ ("6011", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("65", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("644", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("645", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("646", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("647", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("648", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("649", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622126", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622127", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622128", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622129", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62213", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62214", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62215", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62216", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62217", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62218", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62219", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("6222", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("6223", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("6224", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("6225", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("6226", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("6227", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("6228", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62290", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("62291", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622920", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622921", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622922", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622923", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622924", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+ ("622925", CreditCardType.Discover, "Discover", "Discover Financial Services"),
+
+ // JCB: 3528-3589开头,16位
+ ("3528", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+ ("3529", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+ ("353", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+ ("354", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+ ("355", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+ ("356", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+ ("357", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+ ("358", CreditCardType.JCB, "JCB", "JCB Co., Ltd."),
+
+ // Diners Club: 300-305, 309, 36, 38-39开头,14位
+ ("300", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("301", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("302", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("303", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("304", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("305", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("309", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("36", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("38", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+ ("39", CreditCardType.DinersClub, "Diners Club", "Diners Club International"),
+
+ // UnionPay: 62开头,16-19位
+ ("62", CreditCardType.UnionPay, "UnionPay", "China UnionPay"),
+
+ // Maestro: 5018, 5020, 5038, 5893, 6304, 6759, 6761-6763开头,12-19位
+ ("5018", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("5020", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("5038", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("5893", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("6304", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("6759", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("6761", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("6762", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide"),
+ ("6763", CreditCardType.Maestro, "Maestro", "MasterCard Worldwide")
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证信用卡号是否有效(格式 + Luhn校验)
+ ///
+ /// 信用卡号
+ /// 是否有效
+ public static bool IsValid(string? cardNumber)
+ {
+ if (!IsValidFormat(cardNumber))
+ {
+ return false;
+ }
+
+ return ValidateLuhn(cardNumber!);
+ }
+
+ ///
+ /// 验证信用卡号格式
+ ///
+ /// 信用卡号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber))
+ {
+ return false;
+ }
+
+ string cleaned = Regex.Replace(cardNumber, @"\D", "");
+ return CardNumberRegex.IsMatch(cleaned);
+ }
+
+ ///
+ /// 使用Luhn算法验证信用卡号
+ ///
+ /// 信用卡号
+ /// 是否通过Luhn校验
+ public static bool ValidateLuhn(string? cardNumber)
+ {
+ if (string.IsNullOrWhiteSpace(cardNumber))
+ {
+ return false;
+ }
+
+ string cleaned = Regex.Replace(cardNumber, @"\D", "");
+ int sum = 0;
+ int length = cleaned.Length;
+ bool isEvenPosition = false;
+
+ for (int i = length - 1; i >= 0; i--)
+ {
+ if (!char.IsDigit(cleaned[i]))
+ {
+ return false;
+ }
+
+ int digit = cleaned[i] - '0';
+
+ if (isEvenPosition)
+ {
+ digit *= 2;
+ if (digit > 9)
+ {
+ digit -= 9;
+ }
+ }
+
+ sum += digit;
+ isEvenPosition = !isEvenPosition;
+ }
+
+ return sum % 10 == 0;
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取信用卡类型
+ ///
+ /// 信用卡号
+ /// 信用卡类型
+ public static CreditCardType GetCardType(string? cardNumber)
+ {
+ if (!IsValidFormat(cardNumber))
+ {
+ return CreditCardType.Unknown;
+ }
+
+ string cleaned = Regex.Replace(cardNumber!, @"\D", "");
+
+ // 从最长前缀开始匹配
+ for (int len = 6; len >= 1; len--)
+ {
+ if (cleaned.Length < len) continue;
+
+ string prefix = cleaned.Substring(0, len);
+ foreach (var rule in CardTypeRules)
+ {
+ if (rule.Prefix == prefix)
+ {
+ return rule.Type;
+ }
+ }
+ }
+
+ return CreditCardType.Unknown;
+ }
+
+ ///
+ /// 获取信用卡信息
+ ///
+ /// 信用卡号
+ /// 信用卡信息
+ public static CreditCardInfo? GetCardInfo(string? cardNumber)
+ {
+ if (!IsValidFormat(cardNumber))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(cardNumber!, @"\D", "");
+
+ for (int len = 6; len >= 1; len--)
+ {
+ if (cleaned.Length < len) continue;
+
+ string prefix = cleaned.Substring(0, len);
+ foreach (var rule in CardTypeRules)
+ {
+ if (rule.Prefix == prefix)
+ {
+ return new CreditCardInfo
+ {
+ Type = rule.Type,
+ Name = rule.Name,
+ Issuer = rule.Issuer
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取信用卡类型名称
+ ///
+ /// 信用卡号
+ /// 类型名称
+ public static string? GetCardTypeName(string? cardNumber)
+ {
+ return GetCardInfo(cardNumber)?.Name;
+ }
+
+ ///
+ /// 判断是否为Visa卡
+ ///
+ public static bool IsVisa(string? cardNumber) => GetCardType(cardNumber) == CreditCardType.Visa;
+
+ ///
+ /// 判断是否为MasterCard
+ ///
+ public static bool IsMasterCard(string? cardNumber) => GetCardType(cardNumber) == CreditCardType.MasterCard;
+
+ ///
+ /// 判断是否为American Express
+ ///
+ public static bool IsAmex(string? cardNumber) => GetCardType(cardNumber) == CreditCardType.Amex;
+
+ ///
+ /// 判断是否为Discover
+ ///
+ public static bool IsDiscover(string? cardNumber) => GetCardType(cardNumber) == CreditCardType.Discover;
+
+ ///
+ /// 判断是否为JCB
+ ///
+ public static bool IsJCB(string? cardNumber) => GetCardType(cardNumber) == CreditCardType.JCB;
+
+ ///
+ /// 判断是否为银联
+ ///
+ public static bool IsUnionPay(string? cardNumber) => GetCardType(cardNumber) == CreditCardType.UnionPay;
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化信用卡号(每4位一组)
+ ///
+ /// 信用卡号
+ /// 格式化后的卡号
+ public static string? Format(string? cardNumber)
+ {
+ if (!IsValidFormat(cardNumber))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(cardNumber!, @"\D", "");
+ var groups = new List();
+
+ for (int i = 0; i < cleaned.Length; i += 4)
+ {
+ int len = Math.Min(4, cleaned.Length - i);
+ groups.Add(cleaned.Substring(i, len));
+ }
+
+ return string.Join(" ", groups);
+ }
+
+ ///
+ /// 格式化信用卡号(根据卡类型自动选择格式)
+ ///
+ /// 信用卡号
+ /// 格式化后的卡号
+ public static string? FormatByType(string? cardNumber)
+ {
+ if (!IsValidFormat(cardNumber))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(cardNumber!, @"\D", "");
+ CreditCardType type = GetCardType(cleaned);
+
+ // Amex特殊格式:4-6-5
+ if (type == CreditCardType.Amex && cleaned.Length == 15)
+ {
+ return $"{cleaned.Substring(0, 4)} {cleaned.Substring(4, 6)} {cleaned.Substring(10, 5)}";
+ }
+
+ // 默认4位一组
+ return Format(cleaned);
+ }
+
+ ///
+ /// 信用卡号脱敏:**** **** **** 1234
+ ///
+ /// 信用卡号
+ /// 脱敏后的卡号
+ public static string? Mask(string? cardNumber)
+ {
+ if (!IsValidFormat(cardNumber))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(cardNumber!, @"\D", "");
+
+ if (cleaned.Length < 8)
+ {
+ return null;
+ }
+
+ int suffixLen = 4;
+ string suffix = cleaned.Substring(cleaned.Length - suffixLen);
+ int maskLen = cleaned.Length - suffixLen;
+ string masked = new string('*', maskLen);
+
+ // 格式化输出
+ CreditCardType type = GetCardType(cleaned);
+ if (type == CreditCardType.Amex && cleaned.Length == 15)
+ {
+ return $"**** ****** {suffix}";
+ }
+
+ return $"**** **** **** {suffix}";
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/CreditCodeUtil.cs b/EasyTool.Core/BusinessCategory/CreditCodeUtil.cs
new file mode 100644
index 0000000..ad7f1a5
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/CreditCodeUtil.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 统一社会信用代码工具类
+ /// 用于验证和处理中国大陆企业的统一社会信用代码
+ ///
+ public static class CreditCodeUtil
+ {
+ ///
+ /// 统一社会信用代码长度
+ ///
+ private const int CreditCodeLength = 18;
+
+ ///
+ /// 统一社会信用代码字符集(不包含I、O、Z、S、V)
+ ///
+ private const string CreditCodeChars = "0123456789ABCDEFGHJKLMNPQRTUWXY";
+
+ ///
+ /// 校验码权重
+ ///
+ private static readonly int[] Weights = { 1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28 };
+
+ ///
+ /// 验证统一社会信用代码是否有效
+ ///
+ /// 统一社会信用代码
+ /// 是否有效
+ public static bool IsValid(string? creditCode)
+ {
+ if (string.IsNullOrWhiteSpace(creditCode))
+ return false;
+
+ creditCode = creditCode.Trim().ToUpperInvariant();
+
+ // 检查长度
+ if (creditCode.Length != CreditCodeLength)
+ return false;
+
+ // 检查字符是否合法
+ foreach (var c in creditCode)
+ {
+ if (!CreditCodeChars.Contains(c))
+ return false;
+ }
+
+ // 验证校验码
+ return ValidateCheckCode(creditCode);
+ }
+
+ ///
+ /// 获取统一社会信用代码的类型信息
+ ///
+ /// 统一社会信用代码
+ /// 类型信息
+ public static CreditCodeType? GetType(string? creditCode)
+ {
+ if (string.IsNullOrWhiteSpace(creditCode))
+ return null;
+
+ creditCode = creditCode.Trim().ToUpperInvariant();
+
+ if (creditCode.Length != CreditCodeLength)
+ return null;
+
+ var typeCode = creditCode[0];
+ return typeCode switch
+ {
+ '1' => CreditCodeType.Institution,
+ '5' => CreditCodeType.Enterprise,
+ '9' => CreditCodeType.Other,
+ 'Y' => CreditCodeType.IndividualBusiness,
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取登记管理部门
+ ///
+ /// 统一社会信用代码
+ /// 登记管理部门
+ public static string? GetRegistrationAuthority(string? creditCode)
+ {
+ if (string.IsNullOrWhiteSpace(creditCode))
+ return null;
+
+ creditCode = creditCode.Trim().ToUpperInvariant();
+
+ if (creditCode.Length < 2)
+ return null;
+
+ var code = creditCode.Substring(0, 2);
+ return code switch
+ {
+ "11" => "工商行政管理",
+ "12" => "工商行政管理(个体工商户)",
+ "13" => "工商行政管理(农民专业合作社)",
+ "19" => "工商行政管理(其他)",
+ "21" => "机构编制",
+ "31" => "外交",
+ "32" => "文化",
+ "33" => "教育",
+ "34" => "卫生",
+ "35" => "体育",
+ "36" => "新闻出版",
+ "37" => "宗教事务",
+ "41" => "司法行政(律师)",
+ "42" => "司法行政(公证)",
+ "43" => "司法行政(基层法律服务)",
+ "44" => "司法行政(司法鉴定)",
+ "51" => "民政",
+ "52" => "民政(社会组织)",
+ "53" => "民政(基金会)",
+ "54" => "民政(民办非企业单位)",
+ "61" => "旅游",
+ "62" => "文物",
+ "71" => "工会",
+ "81" => "公安",
+ "91" => "其他",
+ "A1" => "全国人大",
+ "A2" => "全国政协",
+ "A3" => "人民法院",
+ "A4" => "人民检察院",
+ "A9" => "其他",
+ "N1" => "军事",
+ "N2" => "武警",
+ _ => "未知"
+ };
+ }
+
+ ///
+ /// 生成校验码
+ ///
+ /// 前17位代码
+ /// 校验码
+ public static char GenerateCheckCode(string creditCode17)
+ {
+ if (string.IsNullOrEmpty(creditCode17) || creditCode17.Length != 17)
+ throw new ArgumentException("输入必须为17位");
+
+ int sum = 0;
+ for (int i = 0; i < 17; i++)
+ {
+ var value = CreditCodeChars.IndexOf(char.ToUpperInvariant(creditCode17[i]));
+ if (value < 0)
+ throw new ArgumentException($"第{i + 1}位字符无效");
+
+ sum += value * Weights[i];
+ }
+
+ var mod = 31 - sum % 31;
+ return mod == 31 ? '0' : CreditCodeChars[mod];
+ }
+
+ private static bool ValidateCheckCode(string creditCode)
+ {
+ var expectedCheckCode = GenerateCheckCode(creditCode.Substring(0, 17));
+ return creditCode[17] == expectedCheckCode;
+ }
+
+ ///
+ /// 格式化统一社会信用代码(添加分隔符)
+ ///
+ /// 统一社会信用代码
+ /// 分隔符
+ /// 格式化后的代码
+ public static string Format(string? creditCode, string separator = "-")
+ {
+ if (string.IsNullOrWhiteSpace(creditCode))
+ return string.Empty;
+
+ creditCode = creditCode.Trim().ToUpperInvariant();
+
+ if (creditCode.Length != CreditCodeLength)
+ return creditCode;
+
+ // 格式:XXXXXX-XXXX-XXXX-XXXX
+ return $"{creditCode.Substring(0, 6)}{separator}{creditCode.Substring(6, 4)}{separator}{creditCode.Substring(10, 4)}{separator}{creditCode.Substring(14, 4)}";
+ }
+ }
+
+ ///
+ /// 统一社会信用代码类型
+ ///
+ public enum CreditCodeType
+ {
+ ///
+ /// 机构
+ ///
+ Institution,
+
+ ///
+ /// 企业
+ ///
+ Enterprise,
+
+ ///
+ /// 其他
+ ///
+ Other,
+
+ ///
+ /// 个体工商户
+ ///
+ IndividualBusiness
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/DomainUtil.cs b/EasyTool.Core/BusinessCategory/DomainUtil.cs
new file mode 100644
index 0000000..7e7c929
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/DomainUtil.cs
@@ -0,0 +1,456 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 域名工具类
+ ///
+ public static class DomainUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 域名正则表达式
+ ///
+ private static readonly Regex DomainRegex = new(
+ @"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// IDN(国际化域名)正则
+ ///
+ private static readonly Regex IdnRegex = new(
+ @"^(?:[a-zA-Z0-9\u4e00-\u9fa5](?:[a-zA-Z0-9-\u4e00-\u9fa5]{0,61}[a-zA-Z0-9\u4e00-\u9fa5])?\.)+[a-zA-Z\u4e00-\u9fa5]{2,}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 顶级域名(TLD)与类型映射
+ ///
+ private static readonly Dictionary TldTypeMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ // 通用顶级域名
+ { "com", "商业机构" }, { "net", "网络服务商" }, { "org", "非营利组织" },
+ { "edu", "教育机构" }, { "gov", "政府机构" }, { "mil", "军事机构" },
+ { "int", "国际组织" }, { "info", "信息服务" }, { "biz", "商业" },
+ { "name", "个人" }, { "pro", "专业人士" }, { "museum", "博物馆" },
+ { "coop", "合作社" }, { "aero", "航空" }, { "xxx", "成人内容" },
+ { "xyz", "通用" }, { "top", "通用" }, { "vip", "VIP" },
+ { "site", "网站" }, { "online", "在线" }, { "store", "商店" },
+ { "tech", "科技" }, { "fun", "娱乐" }, { "club", "俱乐部" },
+ { "shop", "购物" }, { "ltd", "有限公司" }, { "work", "工作" },
+
+ // 国家/地区顶级域名
+ { "cn", "中国" }, { "hk", "香港" }, { "tw", "台湾" }, { "mo", "澳门" },
+ { "jp", "日本" }, { "kr", "韩国" }, { "sg", "新加坡" }, { "my", "马来西亚" },
+ { "th", "泰国" }, { "vn", "越南" }, { "ph", "菲律宾" }, { "id", "印度尼西亚" },
+ { "in", "印度" }, { "pk", "巴基斯坦" }, { "au", "澳大利亚" }, { "nz", "新西兰" },
+ { "us", "美国" }, { "ca", "加拿大" }, { "mx", "墨西哥" }, { "br", "巴西" },
+ { "uk", "英国" }, { "de", "德国" }, { "fr", "法国" }, { "it", "意大利" },
+ { "es", "西班牙" }, { "nl", "荷兰" }, { "be", "比利时" }, { "ch", "瑞士" },
+ { "at", "奥地利" }, { "se", "瑞典" }, { "no", "挪威" }, { "dk", "丹麦" },
+ { "fi", "芬兰" }, { "ru", "俄罗斯" }, { "pl", "波兰" }, { "cz", "捷克" },
+ { "ua", "乌克兰" }, { "tr", "土耳其" }, { "sa", "沙特" }, { "ae", "阿联酋" },
+ { "il", "以色列" }, { "za", "南非" }, { "eg", "埃及" }, { "ng", "尼日利亚" },
+ { "ke", "肯尼亚" }, { "ar", "阿根廷" }, { "cl", "智利" }, { "co", "哥伦比亚" },
+
+ // 中国二级域名
+ { "com.cn", "中国商业" }, { "net.cn", "中国网络" }, { "org.cn", "中国组织" },
+ { "gov.cn", "中国政府" }, { "edu.cn", "中国教育" }, { "ac.cn", "中国科研" },
+ { "mil.cn", "中国军事" }, { "bj.cn", "北京" }, { "sh.cn", "上海" },
+ { "tj.cn", "天津" }, { "cq.cn", "重庆" }, { "he.cn", "河北" },
+ { "sx.cn", "山西" }, { "nm.cn", "内蒙古" }, { "ln.cn", "辽宁" },
+ { "jl.cn", "吉林" }, { "hl.cn", "黑龙江" }, { "js.cn", "江苏" },
+ { "zj.cn", "浙江" }, { "ah.cn", "安徽" }, { "fj.cn", "福建" },
+ { "jx.cn", "江西" }, { "sd.cn", "山东" }, { "ha.cn", "河南" },
+ { "hb.cn", "湖北" }, { "hn.cn", "湖南" }, { "gd.cn", "广东" },
+ { "gx.cn", "广西" }, { "hi.cn", "海南" }, { "sc.cn", "四川" },
+ { "gz.cn", "贵州" }, { "yn.cn", "云南" }, { "xz.cn", "西藏" },
+ { "sn.cn", "陕西" }, { "gs.cn", "甘肃" }, { "qh.cn", "青海" },
+ { "nx.cn", "宁夏" }, { "xj.cn", "新疆" }
+ };
+
+ ///
+ /// 常见二级域名服务
+ ///
+ private static readonly Dictionary WellKnownDomains = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "baidu.com", "百度" }, { "qq.com", "腾讯" }, { "taobao.com", "淘宝" },
+ { "tmall.com", "天猫" }, { "jd.com", "京东" }, { "alipay.com", "支付宝" },
+ { "alibaba.com", "阿里巴巴" }, { "aliyun.com", "阿里云" },
+ { "tencent.com", "腾讯" }, { "weixin.com", "微信" }, { "wechat.com", "微信" },
+ { "douyin.com", "抖音" }, { "tiktok.com", "TikTok" }, { "bytedance.com", "字节跳动" },
+ { "meituan.com", "美团" }, { "dianping.com", "大众点评" },
+ { "didichuxing.com", "滴滴" }, { "xiaojukeji.com", "滴滴" },
+ { "sohu.com", "搜狐" }, { "sina.com.cn", "新浪" }, { "weibo.com", "微博" },
+ { "163.com", "网易" }, { "126.com", "网易" }, { "yeah.net", "网易" },
+ { "zhihu.com", "知乎" }, { "csdn.net", "CSDN" },
+ { "bilibili.com", "哔哩哔哩" }, { "acfun.cn", "AcFun" },
+ { "youku.com", "优酷" }, { "iqiyi.com", "爱奇艺" }, { "v.qq.com", "腾讯视频" },
+ { "github.com", "GitHub" }, { "gitlab.com", "GitLab" }, { "gitee.com", "Gitee" },
+ { "google.com", "Google" }, { "youtube.com", "YouTube" }, { "gmail.com", "Gmail" },
+ { "facebook.com", "Facebook" }, { "instagram.com", "Instagram" }, { "whatsapp.com", "WhatsApp" },
+ { "twitter.com", "Twitter" }, { "x.com", "X" },
+ { "linkedin.com", "LinkedIn" }, { "microsoft.com", "Microsoft" },
+ { "apple.com", "Apple" }, { "amazon.com", "Amazon" }, { "aws.amazon.com", "AWS" },
+ { "cloudflare.com", "Cloudflare" }, { "nginx.com", "NGINX" }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证域名是否有效
+ ///
+ /// 域名
+ /// 是否有效
+ public static bool IsValid(string? domain)
+ {
+ if (string.IsNullOrWhiteSpace(domain))
+ {
+ return false;
+ }
+
+ // 域名总长度不超过253字符
+ if (domain.Length > 253)
+ {
+ return false;
+ }
+
+ string lower = domain.ToLower().Trim();
+
+ // 检查是否为IDN
+ if (IdnRegex.IsMatch(lower))
+ {
+ return true;
+ }
+
+ return DomainRegex.IsMatch(lower);
+ }
+
+ ///
+ /// 验证是否为国际顶级域名
+ ///
+ /// 域名
+ /// 是否为国际域名
+ public static bool IsInternationalDomain(string? domain)
+ {
+ if (!IsValid(domain))
+ {
+ return false;
+ }
+
+ string tld = GetTLD(domain);
+ if (tld == null) return false;
+
+ // 常见国际顶级域名
+ string[] internationalTlds = { "com", "net", "org", "edu", "gov", "mil", "int", "info", "biz", "name", "pro" };
+ foreach (var itld in internationalTlds)
+ {
+ if (tld.Equals(itld, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// 验证是否为中国域名
+ ///
+ /// 域名
+ /// 是否为中国域名
+ public static bool IsChinaDomain(string? domain)
+ {
+ if (!IsValid(domain))
+ {
+ return false;
+ }
+
+ string tld = GetTLD(domain);
+ return tld?.Equals("cn", StringComparison.OrdinalIgnoreCase) == true ||
+ domain!.EndsWith(".com.cn", StringComparison.OrdinalIgnoreCase) ||
+ domain.EndsWith(".net.cn", StringComparison.OrdinalIgnoreCase) ||
+ domain.EndsWith(".org.cn", StringComparison.OrdinalIgnoreCase) ||
+ domain.EndsWith(".gov.cn", StringComparison.OrdinalIgnoreCase) ||
+ domain.EndsWith(".edu.cn", StringComparison.OrdinalIgnoreCase);
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取顶级域名(TLD)
+ ///
+ /// 域名
+ /// 顶级域名
+ public static string? GetTLD(string? domain)
+ {
+ if (!IsValid(domain))
+ {
+ return null;
+ }
+
+ string[] parts = domain!.ToLower().Split('.');
+ if (parts.Length < 2)
+ {
+ return null;
+ }
+
+ // 检查是否为双后缀(如.com.cn)
+ if (parts.Length >= 3)
+ {
+ string possibleDoubleTld = parts[^2] + "." + parts[^1];
+ if (TldTypeMap.ContainsKey(possibleDoubleTld))
+ {
+ return possibleDoubleTld;
+ }
+ }
+
+ return parts[^1];
+ }
+
+ ///
+ /// 获取顶级域名类型/归属
+ ///
+ /// 域名
+ /// 顶级域名类型
+ public static string? GetTLDType(string? domain)
+ {
+ if (!IsValid(domain))
+ {
+ return null;
+ }
+
+ // 先检查双后缀
+ string[] parts = domain!.ToLower().Split('.');
+ if (parts.Length >= 3)
+ {
+ string possibleDoubleTld = parts[^2] + "." + parts[^1];
+ if (TldTypeMap.TryGetValue(possibleDoubleTld, out string? type))
+ {
+ return type;
+ }
+ }
+
+ string tld = GetTLD(domain);
+ if (tld != null && TldTypeMap.TryGetValue(tld, out string? tldType))
+ {
+ return tldType;
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取二级域名
+ ///
+ /// 域名
+ /// 二级域名
+ public static string? GetSecondLevelDomain(string? domain)
+ {
+ if (!IsValid(domain))
+ {
+ return null;
+ }
+
+ string[] parts = domain!.ToLower().Split('.');
+ if (parts.Length < 2)
+ {
+ return null;
+ }
+
+ // 处理双后缀
+ if (parts.Length >= 3)
+ {
+ string possibleDoubleTld = parts[^2] + "." + parts[^1];
+ if (TldTypeMap.ContainsKey(possibleDoubleTld) && parts.Length >= 4)
+ {
+ return parts[^3] + "." + possibleDoubleTld;
+ }
+ }
+
+ return parts[^2] + "." + parts[^1];
+ }
+
+ ///
+ /// 获取子域名前缀
+ ///
+ /// 域名
+ /// 子域名前缀(如www、mail等)
+ public static string? GetSubdomain(string? domain)
+ {
+ if (!IsValid(domain))
+ {
+ return null;
+ }
+
+ string[] parts = domain!.ToLower().Split('.');
+
+ // 计算主域名部分的长度
+ int mainDomainParts = 2;
+ if (parts.Length >= 3)
+ {
+ string possibleDoubleTld = parts[^2] + "." + parts[^1];
+ if (TldTypeMap.ContainsKey(possibleDoubleTld))
+ {
+ mainDomainParts = 3;
+ }
+ }
+
+ if (parts.Length <= mainDomainParts)
+ {
+ return null; // 无子域名
+ }
+
+ // 返回除主域名外的部分
+ return string.Join(".", parts, 0, parts.Length - mainDomainParts);
+ }
+
+ ///
+ /// 获取主域名(不含子域名)
+ ///
+ /// 域名
+ /// 主域名
+ public static string? GetMainDomain(string? domain)
+ {
+ string? sld = GetSecondLevelDomain(domain);
+ return sld;
+ }
+
+ ///
+ /// 获取已知服务名称
+ ///
+ /// 域名
+ /// 服务名称
+ public static string? GetServiceName(string? domain)
+ {
+ if (!IsValid(domain))
+ {
+ return null;
+ }
+
+ string mainDomain = GetMainDomain(domain)?.ToLower() ?? "";
+
+ foreach (var kvp in WellKnownDomains)
+ {
+ if (mainDomain.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase) ||
+ domain!.EndsWith("." + kvp.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ return kvp.Value;
+ }
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化域名(转小写,去除协议和路径)
+ ///
+ /// 域名
+ /// 格式化后的域名
+ public static string? Normalize(string? domain)
+ {
+ if (string.IsNullOrWhiteSpace(domain))
+ {
+ return null;
+ }
+
+ string cleaned = domain.ToLower().Trim();
+
+ // 去除协议
+ if (cleaned.StartsWith("http://"))
+ {
+ cleaned = cleaned.Substring(7);
+ }
+ else if (cleaned.StartsWith("https://"))
+ {
+ cleaned = cleaned.Substring(8);
+ }
+
+ // 去除路径
+ int slashIndex = cleaned.IndexOf('/');
+ if (slashIndex > 0)
+ {
+ cleaned = cleaned.Substring(0, slashIndex);
+ }
+
+ // 去除端口
+ int colonIndex = cleaned.LastIndexOf(':');
+ if (colonIndex > 0)
+ {
+ cleaned = cleaned.Substring(0, colonIndex);
+ }
+
+ return IsValid(cleaned) ? cleaned : null;
+ }
+
+ ///
+ /// 域名脱敏:e*****.com
+ ///
+ /// 域名
+ /// 脱敏后的域名
+ public static string? Mask(string? domain)
+ {
+ string? normalized = Normalize(domain);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ string[] parts = normalized.Split('.');
+ if (parts.Length < 2)
+ {
+ return null;
+ }
+
+ // 脱敏主域名部分
+ string mainPart = parts[0];
+ if (mainPart.Length <= 2)
+ {
+ parts[0] = mainPart[0] + "*";
+ }
+ else
+ {
+ parts[0] = mainPart[0] + new string('*', mainPart.Length - 2) + mainPart[^1];
+ }
+
+ return string.Join(".", parts);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机域名(仅供测试使用)
+ ///
+ /// 顶级域名(可选,默认.com)
+ /// 随机域名
+ public static string GenerateRandom(string? tld = null)
+ {
+ const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ string suffix = tld ?? "com";
+
+ // 生成随机主域名(6-12位)
+ int length = MathCategory.RandomUtil.RandomInt(6, 13);
+ string main = "";
+ for (int i = 0; i < length; i++)
+ {
+ main += MathCategory.RandomUtil.GetRandomElement(chars.ToCharArray());
+ }
+
+ return main + "." + suffix;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/DrivingLicenseUtil.cs b/EasyTool.Core/BusinessCategory/DrivingLicenseUtil.cs
new file mode 100644
index 0000000..b4b834c
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/DrivingLicenseUtil.cs
@@ -0,0 +1,319 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 驾驶证号工具类
+ ///
+ public static class DrivingLicenseUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 驾驶证号正则表达式(18位,与身份证号格式相同)
+ ///
+ private static readonly Regex LicenseRegex = new(
+ @"^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 档案编号正则表达式(12位数字)
+ ///
+ private static readonly Regex FileNumberRegex = new(@"^\d{12}$", RegexOptions.Compiled);
+
+ ///
+ /// 驾驶证校验码权重
+ ///
+ private static readonly int[] Weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
+
+ ///
+ /// 驾驶证校验码对照表
+ ///
+ private static readonly char[] CheckCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };
+
+ ///
+ /// 准驾车型映射
+ ///
+ private static readonly (string Code, string Name, string Description)[] VehicleClassMap =
+ {
+ ("A1", "大型客车", "可驾驶A3、B1、B2、C1、C2、C3、C4、M"),
+ ("A2", "牵引车", "可驾驶B1、B2、C1、C2、C3、C4、M"),
+ ("A3", "城市公交车", "可驾驶C1、C2、C3、C4"),
+ ("B1", "中型客车", "可驾驶C1、C2、C3、C4、M"),
+ ("B2", "大型货车", "可驾驶C1、C2、C3、C4、M"),
+ ("C1", "小型汽车", "可驾驶C2、C3、C4"),
+ ("C2", "小型自动挡汽车", "仅限自动挡小型汽车"),
+ ("C3", "低速载货汽车", "可驾驶C4"),
+ ("C4", "三轮汽车", ""),
+ ("C5", "残疾人专用小型自动挡汽车", ""),
+ ("C6", "轻型牵引挂车", "需C1或C2以上驾照增驾"),
+ ("D", "普通三轮摩托车", "可驾驶E、F"),
+ ("E", "普通二轮摩托车", "可驾驶F"),
+ ("F", "轻便摩托车", ""),
+ ("G", "拖拉机", ""),
+ ("H", "轮式自行机械", ""),
+ ("M", "轮式自行机械车", ""),
+ ("N", "无轨电车", ""),
+ ("P", "有轨电车", "")
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证驾驶证号是否有效
+ ///
+ /// 驾驶证号
+ /// 是否有效
+ public static bool IsValid(string? licenseNumber)
+ {
+ if (string.IsNullOrWhiteSpace(licenseNumber))
+ {
+ return false;
+ }
+
+ if (!LicenseRegex.IsMatch(licenseNumber))
+ {
+ return false;
+ }
+
+ // 验证日期有效性
+ if (!IsValidDate(licenseNumber.Substring(6, 8)))
+ {
+ return false;
+ }
+
+ // 验证校验码
+ int sum = 0;
+ for (int i = 0; i < 17; i++)
+ {
+ sum += (licenseNumber[i] - '0') * Weights[i];
+ }
+
+ char expectedCheckCode = CheckCodes[sum % 11];
+ char actualCheckCode = char.ToUpper(licenseNumber[17]);
+
+ return expectedCheckCode == actualCheckCode;
+ }
+
+ ///
+ /// 验证档案编号是否有效
+ ///
+ /// 档案编号
+ /// 是否有效
+ public static bool IsValidFileNumber(string? fileNumber)
+ {
+ if (string.IsNullOrWhiteSpace(fileNumber))
+ {
+ return false;
+ }
+
+ return FileNumberRegex.IsMatch(fileNumber);
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取出生日期
+ ///
+ /// 驾驶证号
+ /// 出生日期
+ public static DateTime? GetBirthday(string? licenseNumber)
+ {
+ if (!IsValid(licenseNumber))
+ {
+ return null;
+ }
+
+ int year = int.Parse(licenseNumber!.Substring(6, 4));
+ int month = int.Parse(licenseNumber.Substring(10, 2));
+ int day = int.Parse(licenseNumber.Substring(12, 2));
+
+ return new DateTime(year, month, day);
+ }
+
+ ///
+ /// 获取性别(1男2女)
+ ///
+ /// 驾驶证号
+ /// 性别代码
+ public static int? GetGender(string? licenseNumber)
+ {
+ if (!IsValid(licenseNumber))
+ {
+ return null;
+ }
+
+ int genderDigit = licenseNumber![16] - '0';
+ return genderDigit % 2 == 1 ? 1 : 2;
+ }
+
+ ///
+ /// 获取性别字符串
+ ///
+ /// 驾驶证号
+ /// 性别
+ public static string? GetGenderString(string? licenseNumber)
+ {
+ int? gender = GetGender(licenseNumber);
+ return gender switch
+ {
+ 1 => "男",
+ 2 => "女",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取行政区划代码
+ ///
+ /// 驾驶证号
+ /// 行政区划代码
+ public static string? GetAreaCode(string? licenseNumber)
+ {
+ if (!IsValid(licenseNumber))
+ {
+ return null;
+ }
+
+ return licenseNumber!.Substring(0, 6);
+ }
+
+ ///
+ /// 判断驾驶证号是否与身份证号一致
+ ///
+ /// 驾驶证号
+ /// 身份证号
+ /// 是否一致
+ public static bool MatchesIdCard(string? licenseNumber, string? idCard)
+ {
+ if (!IsValid(licenseNumber) || !IdCardUtil.IsValid18(idCard))
+ {
+ return false;
+ }
+
+ return licenseNumber!.Equals(idCard!, StringComparison.OrdinalIgnoreCase);
+ }
+
+ #endregion
+
+ #region 准驾车型
+
+ ///
+ /// 获取准驾车型信息
+ ///
+ /// 准驾车型代码
+ /// 车型信息
+ public static (string Name, string Description)? GetVehicleClassInfo(string? vehicleClass)
+ {
+ if (string.IsNullOrWhiteSpace(vehicleClass))
+ {
+ return null;
+ }
+
+ foreach (var info in VehicleClassMap)
+ {
+ if (info.Code.Equals(vehicleClass, StringComparison.OrdinalIgnoreCase))
+ {
+ return (info.Name, info.Description);
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取准驾车型名称
+ ///
+ /// 准驾车型代码
+ /// 车型名称
+ public static string? GetVehicleClassName(string? vehicleClass)
+ {
+ var info = GetVehicleClassInfo(vehicleClass);
+ return info?.Name;
+ }
+
+ ///
+ /// 验证准驾车型代码是否有效
+ ///
+ /// 准驾车型代码
+ /// 是否有效
+ public static bool IsValidVehicleClass(string? vehicleClass)
+ {
+ return GetVehicleClassInfo(vehicleClass) != null;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化驾驶证号(转大写)
+ ///
+ /// 驾驶证号
+ /// 格式化后的驾驶证号
+ public static string? Normalize(string? licenseNumber)
+ {
+ if (string.IsNullOrWhiteSpace(licenseNumber))
+ {
+ return null;
+ }
+
+ string upper = licenseNumber.ToUpper().Trim();
+ return upper.Length == 18 && LicenseRegex.IsMatch(upper) ? upper : null;
+ }
+
+ ///
+ /// 驾驶证号脱敏:110***********1234
+ ///
+ /// 驾驶证号
+ /// 脱敏后的驾驶证号
+ public static string? Mask(string? licenseNumber)
+ {
+ if (!IsValid(licenseNumber))
+ {
+ return null;
+ }
+
+ return licenseNumber!.Substring(0, 3) + "***********" + licenseNumber.Substring(14);
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 验证日期字符串是否有效
+ ///
+ private static bool IsValidDate(string dateStr)
+ {
+ if (dateStr.Length != 8)
+ {
+ return false;
+ }
+
+ int year = int.Parse(dateStr.Substring(0, 4));
+ int month = int.Parse(dateStr.Substring(4, 2));
+ int day = int.Parse(dateStr.Substring(6, 2));
+
+ if (year < 1900 || year > DateTime.Now.Year)
+ {
+ return false;
+ }
+
+ if (month < 1 || month > 12)
+ {
+ return false;
+ }
+
+ int maxDay = DateTime.DaysInMonth(year, month);
+ return day >= 1 && day <= maxDay;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/EmailUtil.cs b/EasyTool.Core/BusinessCategory/EmailUtil.cs
new file mode 100644
index 0000000..903a7f3
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/EmailUtil.cs
@@ -0,0 +1,518 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 邮箱服务提供商枚举
+ ///
+ public enum EmailProvider
+ {
+ ///
+ /// 未知服务商
+ ///
+ Unknown = 0,
+
+ ///
+ /// QQ邮箱
+ ///
+ QQ = 1,
+
+ ///
+ /// 网易163邮箱
+ ///
+ NetEase163 = 2,
+
+ ///
+ /// 网易126邮箱
+ ///
+ NetEase126 = 3,
+
+ ///
+ /// 网易yeah邮箱
+ ///
+ NetEaseYeah = 4,
+
+ ///
+ /// 新浪邮箱
+ ///
+ Sina = 5,
+
+ ///
+ /// 搜狐邮箱
+ ///
+ Sohu = 6,
+
+ ///
+ /// Gmail
+ ///
+ Gmail = 7,
+
+ ///
+ /// Outlook/Hotmail
+ ///
+ Outlook = 8,
+
+ ///
+ /// Yahoo
+ ///
+ Yahoo = 9,
+
+ ///
+ /// iCloud
+ ///
+ ICloud = 10,
+
+ ///
+ /// 阿里云邮箱
+ ///
+ Aliyun = 11,
+
+ ///
+ /// 企业邮箱
+ ///
+ Enterprise = 12
+ }
+
+ ///
+ /// 邮箱工具类
+ ///
+ public static class EmailUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 邮箱正则表达式(标准格式)
+ ///
+ private static readonly Regex EmailRegex = new Regex(
+ @"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 简单邮箱正则表达式(用于快速验证)
+ ///
+ private static readonly Regex SimpleEmailRegex = new Regex(
+ @"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 邮箱服务商域名映射
+ ///
+ private static readonly Dictionary ProviderDomainMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // QQ邮箱
+ { "qq.com", EmailProvider.QQ },
+ { "foxmail.com", EmailProvider.QQ },
+ { "vip.qq.com", EmailProvider.QQ },
+
+ // 网易邮箱
+ { "163.com", EmailProvider.NetEase163 },
+ { "vip.163.com", EmailProvider.NetEase163 },
+ { "126.com", EmailProvider.NetEase126 },
+ { "vip.126.com", EmailProvider.NetEase126 },
+ { "yeah.net", EmailProvider.NetEaseYeah },
+
+ // 新浪邮箱
+ { "sina.com", EmailProvider.Sina },
+ { "sina.cn", EmailProvider.Sina },
+ { "vip.sina.com", EmailProvider.Sina },
+
+ // 搜狐邮箱
+ { "sohu.com", EmailProvider.Sohu },
+ { "vip.sohu.com", EmailProvider.Sohu },
+
+ // Gmail
+ { "gmail.com", EmailProvider.Gmail },
+ { "googlemail.com", EmailProvider.Gmail },
+
+ // Outlook/Hotmail
+ { "outlook.com", EmailProvider.Outlook },
+ { "hotmail.com", EmailProvider.Outlook },
+ { "live.com", EmailProvider.Outlook },
+ { "msn.com", EmailProvider.Outlook },
+
+ // Yahoo
+ { "yahoo.com", EmailProvider.Yahoo },
+ { "yahoo.cn", EmailProvider.Yahoo },
+ { "yahoo.com.cn", EmailProvider.Yahoo },
+ { "yahoo.co.jp", EmailProvider.Yahoo },
+ { "ymail.com", EmailProvider.Yahoo },
+
+ // iCloud
+ { "icloud.com", EmailProvider.ICloud },
+ { "me.com", EmailProvider.ICloud },
+ { "mac.com", EmailProvider.ICloud },
+
+ // 阿里云邮箱
+ { "aliyun.com", EmailProvider.Aliyun },
+ { "aliyuncs.com", EmailProvider.Aliyun }
+ };
+
+ ///
+ /// 常见企业邮箱域名
+ ///
+ private static readonly HashSet EnterpriseDomains = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "exmail.qq.com", // 腾讯企业邮
+ "qiye.163.com", // 网易企业邮
+ "qiye.aliyun.com", // 阿里企业邮
+ "corp.sina.com", // 新浪企业邮
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证邮箱格式是否有效(标准验证)
+ ///
+ /// 邮箱地址
+ /// 是否有效
+ public static bool IsValid(string? email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ return false;
+ }
+
+ // 长度检查(RFC 5321规定最大254字符)
+ if (email.Length > 254)
+ {
+ return false;
+ }
+
+ return EmailRegex.IsMatch(email);
+ }
+
+ ///
+ /// 快速验证邮箱格式(简单验证,性能更好)
+ ///
+ /// 邮箱地址
+ /// 是否有效
+ public static bool IsValidQuick(string? email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ return false;
+ }
+
+ if (email.Length > 254)
+ {
+ return false;
+ }
+
+ return SimpleEmailRegex.IsMatch(email);
+ }
+
+ ///
+ /// 验证邮箱格式并规范化
+ ///
+ /// 邮箱地址
+ /// 规范化后的邮箱地址,无效返回null
+ public static string? Normalize(string? email)
+ {
+ if (!IsValid(email))
+ {
+ return null;
+ }
+
+ // 转小写,去除首尾空格
+ return email!.Trim().ToLower();
+ }
+
+ #endregion
+
+ #region 解析方法
+
+ ///
+ /// 获取邮箱用户名(@之前的部分)
+ ///
+ /// 邮箱地址
+ /// 用户名,无效邮箱返回null
+ public static string? GetUsername(string? email)
+ {
+ if (!IsValidQuick(email))
+ {
+ return null;
+ }
+
+ int atIndex = email!.IndexOf('@');
+ return email.Substring(0, atIndex);
+ }
+
+ ///
+ /// 获取邮箱域名(@之后的部分)
+ ///
+ /// 邮箱地址
+ /// 域名,无效邮箱返回null
+ public static string? GetDomain(string? email)
+ {
+ if (!IsValidQuick(email))
+ {
+ return null;
+ }
+
+ int atIndex = email!.IndexOf('@');
+ return email.Substring(atIndex + 1).ToLower();
+ }
+
+ ///
+ /// 获取邮箱顶级域名
+ ///
+ /// 邮箱地址
+ /// 顶级域名(如.com、.cn),无效邮箱返回null
+ public static string? GetTopLevelDomain(string? email)
+ {
+ string? domain = GetDomain(email);
+ if (domain == null)
+ {
+ return null;
+ }
+
+ int lastDotIndex = domain.LastIndexOf('.');
+ if (lastDotIndex < 0)
+ {
+ return null;
+ }
+
+ return domain.Substring(lastDotIndex);
+ }
+
+ #endregion
+
+ #region 服务商识别
+
+ ///
+ /// 获取邮箱服务商
+ ///
+ /// 邮箱地址
+ /// 邮箱服务商枚举
+ public static EmailProvider GetProvider(string? email)
+ {
+ string? domain = GetDomain(email);
+ if (domain == null)
+ {
+ return EmailProvider.Unknown;
+ }
+
+ // 检查企业邮箱
+ if (EnterpriseDomains.Contains(domain))
+ {
+ return EmailProvider.Enterprise;
+ }
+
+ // 检查已知服务商
+ if (ProviderDomainMap.TryGetValue(domain, out EmailProvider provider))
+ {
+ return provider;
+ }
+
+ // 检查子域名(如 vip.qq.com)
+ foreach (var kvp in ProviderDomainMap)
+ {
+ if (domain.EndsWith("." + kvp.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ return kvp.Value;
+ }
+ }
+
+ return EmailProvider.Unknown;
+ }
+
+ ///
+ /// 获取邮箱服务商名称
+ ///
+ /// 邮箱地址
+ /// 服务商名称
+ public static string? GetProviderName(string? email)
+ {
+ EmailProvider provider = GetProvider(email);
+ return provider switch
+ {
+ EmailProvider.QQ => "QQ邮箱",
+ EmailProvider.NetEase163 => "163邮箱",
+ EmailProvider.NetEase126 => "126邮箱",
+ EmailProvider.NetEaseYeah => "Yeah邮箱",
+ EmailProvider.Sina => "新浪邮箱",
+ EmailProvider.Sohu => "搜狐邮箱",
+ EmailProvider.Gmail => "Gmail",
+ EmailProvider.Outlook => "Outlook",
+ EmailProvider.Yahoo => "Yahoo邮箱",
+ EmailProvider.ICloud => "iCloud",
+ EmailProvider.Aliyun => "阿里云邮箱",
+ EmailProvider.Enterprise => "企业邮箱",
+ _ => null
+ };
+ }
+
+ ///
+ /// 判断是否为企业邮箱
+ ///
+ /// 邮箱地址
+ /// 是否为企业邮箱
+ public static bool IsEnterpriseEmail(string? email)
+ {
+ EmailProvider provider = GetProvider(email);
+
+ // 已知企业邮箱域名
+ if (provider == EmailProvider.Enterprise)
+ {
+ return true;
+ }
+
+ // 未知服务商可能是企业邮箱
+ if (provider == EmailProvider.Unknown)
+ {
+ string? domain = GetDomain(email);
+ // 排除常见个人邮箱域名后的其他域名可能是企业邮箱
+ return domain != null && !IsCommonPublicDomain(domain);
+ }
+
+ return false;
+ }
+
+ ///
+ /// 判断是否为常见公共邮箱域名
+ ///
+ private static bool IsCommonPublicDomain(string domain)
+ {
+ return ProviderDomainMap.ContainsKey(domain);
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 邮箱脱敏:t***@qq.com
+ ///
+ /// 邮箱地址
+ /// 脱敏后的邮箱地址
+ public static string? Mask(string? email)
+ {
+ if (!IsValidQuick(email))
+ {
+ return null;
+ }
+
+ int atIndex = email!.IndexOf('@');
+ string username = email.Substring(0, atIndex);
+ string domain = email.Substring(atIndex);
+
+ if (username.Length <= 1)
+ {
+ return "*" + domain;
+ }
+ else if (username.Length <= 3)
+ {
+ return username[0] + new string('*', username.Length - 1) + domain;
+ }
+ else
+ {
+ return username.Substring(0, 2) + new string('*', username.Length - 2) + domain;
+ }
+ }
+
+ ///
+ /// 邮箱脱敏(自定义脱敏字符数)
+ ///
+ /// 邮箱地址
+ /// 用户名可见字符数
+ /// 脱敏后的邮箱地址
+ public static string? Mask(string? email, int visibleChars)
+ {
+ if (!IsValidQuick(email))
+ {
+ return null;
+ }
+
+ int atIndex = email!.IndexOf('@');
+ string username = email.Substring(0, atIndex);
+ string domain = email.Substring(atIndex);
+
+ if (visibleChars <= 0)
+ {
+ return new string('*', Math.Min(username.Length, 3)) + domain;
+ }
+
+ if (visibleChars >= username.Length)
+ {
+ return email;
+ }
+
+ return username.Substring(0, visibleChars) + new string('*', username.Length - visibleChars) + domain;
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机邮箱(仅供测试使用)
+ ///
+ /// 邮箱服务商(可选,默认随机)
+ /// 随机邮箱地址
+ public static string GenerateRandom(EmailProvider? provider = null)
+ {
+ string username = GenerateRandomUsername(8);
+ string domain;
+
+ if (provider.HasValue && provider.Value != EmailProvider.Unknown && provider.Value != EmailProvider.Enterprise)
+ {
+ domain = GetDomainByProvider(provider.Value);
+ }
+ else
+ {
+ // 随机选择一个服务商
+ var providers = new[] { EmailProvider.QQ, EmailProvider.NetEase163, EmailProvider.Gmail, EmailProvider.Outlook };
+ var randomProvider = EasyTool.MathCategory.RandomUtil.GetRandomElement(providers);
+ domain = GetDomainByProvider(randomProvider);
+ }
+
+ return username + "@" + domain;
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 根据服务商获取域名
+ ///
+ private static string GetDomainByProvider(EmailProvider provider)
+ {
+ return provider switch
+ {
+ EmailProvider.QQ => "qq.com",
+ EmailProvider.NetEase163 => "163.com",
+ EmailProvider.NetEase126 => "126.com",
+ EmailProvider.NetEaseYeah => "yeah.net",
+ EmailProvider.Sina => "sina.com",
+ EmailProvider.Sohu => "sohu.com",
+ EmailProvider.Gmail => "gmail.com",
+ EmailProvider.Outlook => "outlook.com",
+ EmailProvider.Yahoo => "yahoo.com",
+ EmailProvider.ICloud => "icloud.com",
+ EmailProvider.Aliyun => "aliyun.com",
+ _ => "example.com"
+ };
+ }
+
+ ///
+ /// 生成随机用户名
+ ///
+ private static string GenerateRandomUsername(int length)
+ {
+ const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ var sb = new System.Text.StringBuilder(length);
+ for (int i = 0; i < length; i++)
+ {
+ sb.Append(EasyTool.MathCategory.RandomUtil.GetRandomElement(chars.ToCharArray()));
+ }
+ return sb.ToString();
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/ForeignerIdUtil.cs b/EasyTool.Core/BusinessCategory/ForeignerIdUtil.cs
new file mode 100644
index 0000000..142e842
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/ForeignerIdUtil.cs
@@ -0,0 +1,368 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 外国人永久居留身份证工具类
+ ///
+ public static class ForeignerIdUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 外国人永久居留身份证正则表达式(15位)
+ ///
+ private static readonly Regex ForeignerId15Regex = new(
+ @"^[A-Z]{3}\d{12}$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 新版外国人永久居留身份证正则表达式(18位)
+ /// 格式与普通身份证相同,但用于外国人
+ ///
+ private static readonly Regex ForeignerId18Regex = new(
+ @"^[A-Z]\d{17}$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 校验码权重(18位版本)
+ ///
+ private static readonly int[] Weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
+
+ ///
+ /// 校验码对照表(18位版本)
+ ///
+ private static readonly char[] CheckCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };
+
+ ///
+ /// 国籍代码映射(部分常见国家)
+ ///
+ private static readonly Dictionary NationalityMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "USA", "美国" }, { "GBR", "英国" }, { "JPN", "日本" }, { "KOR", "韩国" },
+ { "DEU", "德国" }, { "FRA", "法国" }, { "ITA", "意大利" }, { "ESP", "西班牙" },
+ { "CAN", "加拿大" }, { "AUS", "澳大利亚" }, { "NZL", "新西兰" }, { "RUS", "俄罗斯" },
+ { "IND", "印度" }, { "THA", "泰国" }, { "VNM", "越南" }, { "MYS", "马来西亚" },
+ { "SGP", "新加坡" }, { "IDN", "印度尼西亚" }, { "PHL", "菲律宾" }, { "MMR", "缅甸" },
+ { "PAK", "巴基斯坦" }, { "BGD", "孟加拉国" }, { "BRA", "巴西" }, { "MEX", "墨西哥" },
+ { "ZAF", "南非" }, { "EGY", "埃及" }, { "NGA", "尼日利亚" }, { "KEN", "肯尼亚" },
+ { "CHN", "中国" }, { "HKG", "香港" }, { "MAC", "澳门" }, { "TWN", "台湾" }
+ };
+
+ ///
+ /// 省份代码映射
+ ///
+ private static readonly Dictionary ProvinceCodeMap = new()
+ {
+ { "11", "北京" }, { "12", "天津" }, { "13", "河北" }, { "14", "山西" },
+ { "15", "内蒙古" }, { "21", "辽宁" }, { "22", "吉林" }, { "23", "黑龙江" },
+ { "31", "上海" }, { "32", "江苏" }, { "33", "浙江" }, { "34", "安徽" },
+ { "35", "福建" }, { "36", "江西" }, { "37", "山东" }, { "41", "河南" },
+ { "42", "湖北" }, { "43", "湖南" }, { "44", "广东" }, { "45", "广西" },
+ { "46", "海南" }, { "50", "重庆" }, { "51", "四川" }, { "52", "贵州" },
+ { "53", "云南" }, { "54", "西藏" }, { "61", "陕西" }, { "62", "甘肃" },
+ { "63", "青海" }, { "64", "宁夏" }, { "65", "新疆" }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证外国人永久居留身份证是否有效
+ ///
+ /// 外国人永久居留身份证号
+ /// 是否有效
+ public static bool IsValid(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ string cleaned = idCard.ToUpper().Trim();
+
+ // 15位格式(旧版)
+ if (cleaned.Length == 15 && ForeignerId15Regex.IsMatch(cleaned))
+ {
+ return true;
+ }
+
+ // 18位格式(新版)
+ if (cleaned.Length == 18 && ForeignerId18Regex.IsMatch(cleaned))
+ {
+ return ValidateCheckDigit18(cleaned);
+ }
+
+ return false;
+ }
+
+ ///
+ /// 验证格式是否正确(不校验校验位)
+ ///
+ /// 外国人永久居留身份证号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ string cleaned = idCard.ToUpper().Trim();
+ return ForeignerId15Regex.IsMatch(cleaned) || ForeignerId18Regex.IsMatch(cleaned);
+ }
+
+ ///
+ /// 验证是否为15位格式
+ ///
+ /// 外国人永久居留身份证号
+ /// 是否为15位格式
+ public static bool Is15Digit(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ return ForeignerId15Regex.IsMatch(idCard.ToUpper().Trim());
+ }
+
+ ///
+ /// 验证是否为18位格式
+ ///
+ /// 外国人永久居留身份证号
+ /// 是否为18位格式
+ public static bool Is18Digit(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ string cleaned = idCard.ToUpper().Trim();
+ return cleaned.Length == 18 && ForeignerId18Regex.IsMatch(cleaned);
+ }
+
+ ///
+ /// 验证18位校验码
+ ///
+ private static bool ValidateCheckDigit18(string idCard)
+ {
+ if (idCard.Length != 18)
+ {
+ return false;
+ }
+
+ // 字母转换(A=10, B=11, ..., Z=35)
+ char firstChar = char.ToUpper(idCard[0]);
+ int firstValue;
+ if (firstChar >= 'A' && firstChar <= 'Z')
+ {
+ firstValue = firstChar - 'A' + 10;
+ }
+ else
+ {
+ return false;
+ }
+
+ // 计算加权和
+ int sum = 0;
+
+ // 第一位字母的权重处理
+ sum += (firstValue / 10) * Weights[0];
+ sum += (firstValue % 10) * Weights[1];
+
+ // 数字部分
+ for (int i = 1; i < 17; i++)
+ {
+ if (!char.IsDigit(idCard[i]))
+ {
+ return false;
+ }
+ sum += (idCard[i] - '0') * Weights[i + 1];
+ }
+
+ // 计算校验码
+ char expectedCheck = CheckCodes[sum % 11];
+ return char.ToUpper(idCard[17]) == expectedCheck;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取国籍代码(15位格式前3位)
+ ///
+ /// 外国人永久居留身份证号
+ /// 国籍代码
+ public static string? GetNationalityCode(string? idCard)
+ {
+ if (Is15Digit(idCard))
+ {
+ return idCard!.Substring(0, 3).ToUpper();
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取国籍名称
+ ///
+ /// 外国人永久居留身份证号
+ /// 国籍名称
+ public static string? GetNationality(string? idCard)
+ {
+ string? code = GetNationalityCode(idCard);
+ if (code == null)
+ {
+ return null;
+ }
+
+ return NationalityMap.TryGetValue(code, out string? nationality) ? nationality : code;
+ }
+
+ ///
+ /// 获取省份代码(18位格式的第2-3位)
+ ///
+ /// 外国人永久居留身份证号
+ /// 省份代码
+ public static string? GetProvinceCode(string? idCard)
+ {
+ if (!Is18Digit(idCard))
+ {
+ return null;
+ }
+
+ return idCard!.Substring(1, 2);
+ }
+
+ ///
+ /// 获取省份名称
+ ///
+ /// 外国人永久居留身份证号
+ /// 省份名称
+ public static string? GetProvince(string? idCard)
+ {
+ string? code = GetProvinceCode(idCard);
+ if (code == null)
+ {
+ return null;
+ }
+
+ return ProvinceCodeMap.TryGetValue(code, out string? province) ? province : null;
+ }
+
+ ///
+ /// 获取出生日期(18位格式)
+ ///
+ /// 外国人永久居留身份证号
+ /// 出生日期
+ public static DateTime? GetBirthday(string? idCard)
+ {
+ if (!Is18Digit(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.Substring(1); // 去掉首字母
+ int year = int.Parse(cleaned.Substring(5, 4));
+ int month = int.Parse(cleaned.Substring(9, 2));
+ int day = int.Parse(cleaned.Substring(11, 2));
+
+ try
+ {
+ return new DateTime(year, month, day);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 获取性别(18位格式)
+ ///
+ /// 外国人永久居留身份证号
+ /// 性别(1男2女)
+ public static int? GetGender(string? idCard)
+ {
+ if (!Is18Digit(idCard))
+ {
+ return null;
+ }
+
+ int genderDigit = idCard![16] - '0';
+ return genderDigit % 2 == 1 ? 1 : 2;
+ }
+
+ ///
+ /// 获取性别字符串
+ ///
+ /// 外国人永久居留身份证号
+ /// 性别
+ public static string? GetGenderString(string? idCard)
+ {
+ int? gender = GetGender(idCard);
+ return gender switch
+ {
+ 1 => "男",
+ 2 => "女",
+ _ => null
+ };
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化外国人永久居留身份证(统一大写)
+ ///
+ /// 外国人永久居留身份证号
+ /// 格式化后的身份证号
+ public static string? Normalize(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ return idCard!.ToUpper().Trim();
+ }
+
+ ///
+ /// 外国人永久居留身份证脱敏
+ /// 15位:USA*********
+ /// 18位:A110**********1
+ ///
+ /// 外国人永久居留身份证号
+ /// 脱敏后的身份证号
+ public static string? Mask(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.ToUpper().Trim();
+
+ if (cleaned.Length == 15)
+ {
+ return cleaned.Substring(0, 3) + "***********" + cleaned.Substring(14);
+ }
+
+ if (cleaned.Length == 18)
+ {
+ return cleaned.Substring(0, 4) + "***********" + cleaned.Substring(15);
+ }
+
+ return null;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/HKIdCardUtil.cs b/EasyTool.Core/BusinessCategory/HKIdCardUtil.cs
new file mode 100644
index 0000000..295bbbb
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/HKIdCardUtil.cs
@@ -0,0 +1,382 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 香港身份证工具类
+ ///
+ public static class HKIdCardUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 香港身份证正则表达式
+ /// 格式:1-2个英文字母 + 6位数字 + 括号内1位校验码
+ /// 例如:A123456(7), AB123456(7)
+ ///
+ private static readonly Regex HKIdCardRegex = new(
+ @"^[A-Z]{1,2}\d{6}\([\dA]\)$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 香港身份证前缀与含义映射
+ ///
+ private static readonly string[] PrefixMeanings = new string[]
+ {
+ "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R", "S", "T", "V", "W", "Y", "Z"
+ };
+
+ ///
+ /// 首字母对应的数字值(A=1, B=2, ..., Z=26)
+ ///
+ private static int GetLetterValue(char letter)
+ {
+ return char.ToUpper(letter) - 'A' + 1;
+ }
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证香港身份证是否有效
+ ///
+ /// 香港身份证号
+ /// 是否有效
+ public static bool IsValid(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ string cleaned = idCard.ToUpper().Trim();
+
+ // 检查格式
+ if (!HKIdCardRegex.IsMatch(cleaned))
+ {
+ return false;
+ }
+
+ // 验证校验码
+ return ValidateCheckDigit(cleaned);
+ }
+
+ ///
+ /// 验证格式是否正确(不校验校验位)
+ ///
+ /// 香港身份证号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ return HKIdCardRegex.IsMatch(idCard.ToUpper().Trim());
+ }
+
+ ///
+ /// 验证校验码
+ ///
+ private static bool ValidateCheckDigit(string idCard)
+ {
+ // 提取校验码(括号内的字符)
+ int parenStart = idCard.IndexOf('(');
+ int parenEnd = idCard.IndexOf(')');
+ if (parenStart < 0 || parenEnd < 0 || parenEnd <= parenStart)
+ {
+ return false;
+ }
+
+ string checkChar = idCard.Substring(parenStart + 1, parenEnd - parenStart - 1);
+ if (checkChar.Length != 1)
+ {
+ return false;
+ }
+
+ // 计算校验码
+ char? expectedCheck = CalculateCheckDigit(idCard);
+ if (expectedCheck == null)
+ {
+ return false;
+ }
+
+ return char.ToUpper(checkChar[0]) == expectedCheck.Value;
+ }
+
+ ///
+ /// 计算校验码
+ ///
+ /// 香港身份证号(含括号格式)
+ /// 校验码字符
+ public static char? CalculateCheckDigit(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard.ToUpper().Trim();
+
+ // 提取字母部分和数字部分
+ int digitStart = -1;
+ for (int i = 0; i < cleaned.Length; i++)
+ {
+ if (char.IsDigit(cleaned[i]))
+ {
+ digitStart = i;
+ break;
+ }
+ }
+
+ if (digitStart < 0)
+ {
+ return null;
+ }
+
+ string letters = cleaned.Substring(0, digitStart);
+ string digits = cleaned.Substring(digitStart, 6);
+
+ // 计算加权和
+ int sum = 0;
+ int weight = 9 - (2 - letters.Length); // 根据字母数量调整起始权重
+
+ // 如果只有一个字母,第一位按36处理(相当于前面有一个空位,值为36)
+ if (letters.Length == 1)
+ {
+ sum += 36 * 9;
+ sum += GetLetterValue(letters[0]) * 8;
+ }
+ else if (letters.Length == 2)
+ {
+ sum += GetLetterValue(letters[0]) * 9;
+ sum += GetLetterValue(letters[1]) * 8;
+ }
+ else
+ {
+ return null;
+ }
+
+ // 数字部分权重为7到2
+ int[] digitWeights = { 7, 6, 5, 4, 3, 2 };
+ for (int i = 0; i < 6; i++)
+ {
+ sum += (digits[i] - '0') * digitWeights[i];
+ }
+
+ // 计算校验码
+ int remainder = sum % 11;
+ int checkValue;
+
+ if (remainder == 0)
+ {
+ checkValue = 0;
+ }
+ else
+ {
+ checkValue = 11 - remainder;
+ }
+
+ // 返回校验码字符
+ if (checkValue == 10)
+ {
+ return 'A';
+ }
+ else
+ {
+ return (char)('0' + checkValue);
+ }
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取身份证前缀字母
+ ///
+ /// 香港身份证号
+ /// 前缀字母
+ public static string? GetPrefix(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.ToUpper().Trim();
+ int digitStart = -1;
+ for (int i = 0; i < cleaned.Length; i++)
+ {
+ if (char.IsDigit(cleaned[i]))
+ {
+ digitStart = i;
+ break;
+ }
+ }
+
+ return digitStart > 0 ? cleaned.Substring(0, digitStart) : null;
+ }
+
+ ///
+ /// 获取数字部分(6位)
+ ///
+ /// 香港身份证号
+ /// 6位数字
+ public static string? GetDigitPart(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.ToUpper().Trim();
+ int digitStart = -1;
+ for (int i = 0; i < cleaned.Length; i++)
+ {
+ if (char.IsDigit(cleaned[i]))
+ {
+ digitStart = i;
+ break;
+ }
+ }
+
+ return digitStart >= 0 ? cleaned.Substring(digitStart, 6) : null;
+ }
+
+ ///
+ /// 获取校验码
+ ///
+ /// 香港身份证号
+ /// 校验码字符
+ public static char? GetCheckDigit(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.ToUpper().Trim();
+ int parenStart = cleaned.IndexOf('(');
+ int parenEnd = cleaned.IndexOf(')');
+
+ if (parenStart < 0 || parenEnd < 0 || parenEnd <= parenStart)
+ {
+ return null;
+ }
+
+ return cleaned[parenStart + 1];
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化香港身份证(统一大写,带括号)
+ ///
+ /// 香港身份证号
+ /// 格式化后的身份证号
+ public static string? Normalize(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ return idCard!.ToUpper().Trim();
+ }
+
+ ///
+ /// 格式化为标准格式(确保括号正确)
+ ///
+ /// 香港身份证号
+ /// 标准格式的身份证号
+ public static string? Format(string? idCard)
+ {
+ if (!IsValid(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.ToUpper().Trim();
+ return cleaned;
+ }
+
+ ///
+ /// 香港身份证脱敏:A12***(7)
+ ///
+ /// 香港身份证号
+ /// 脱敏后的身份证号
+ public static string? Mask(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.ToUpper().Trim();
+ int parenStart = cleaned.IndexOf('(');
+
+ if (parenStart < 7)
+ {
+ return null;
+ }
+
+ // 保留前缀+2位数字,中间用*替代,保留校验码
+ string prefix = cleaned.Substring(0, parenStart - 4);
+ string suffix = cleaned.Substring(parenStart);
+
+ return prefix + "****" + suffix;
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机香港身份证号(仅供测试使用)
+ ///
+ /// 前缀字母(可选,默认随机)
+ /// 香港身份证号
+ public static string GenerateRandom(string? prefix = null)
+ {
+ const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ const string digits = "0123456789";
+
+ // 前缀
+ string prefixLetters;
+ if (string.IsNullOrEmpty(prefix))
+ {
+ int letterCount = MathCategory.RandomUtil.RandomInt(1, 3);
+ prefixLetters = "";
+ for (int i = 0; i < letterCount; i++)
+ {
+ prefixLetters += MathCategory.RandomUtil.GetRandomElement(letters.ToCharArray());
+ }
+ }
+ else
+ {
+ prefixLetters = prefix.ToUpper();
+ }
+
+ // 6位数字
+ string numberPart = "";
+ for (int i = 0; i < 6; i++)
+ {
+ numberPart += MathCategory.RandomUtil.GetRandomElement(digits.ToCharArray());
+ }
+
+ // 计算校验码
+ string tempId = prefixLetters + numberPart + "(0)";
+ char? checkDigit = CalculateCheckDigit(tempId);
+
+ return $"{prefixLetters}{numberPart}({checkDigit ?? '0'})";
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/ICCIDUtil.cs b/EasyTool.Core/BusinessCategory/ICCIDUtil.cs
new file mode 100644
index 0000000..0c5ba59
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/ICCIDUtil.cs
@@ -0,0 +1,412 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// SIM卡ICCID工具类
+ /// ICCID (Integrated Circuit Card Identifier) 是SIM卡的唯一识别号
+ ///
+ public static class ICCIDUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// ICCID正则表达式(19-20位数字)
+ ///
+ private static readonly Regex ICCIDRegex = new(
+ @"^\d{19,20}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 移动国家代码(MCC)映射
+ ///
+ private static readonly Dictionary MccMap = new()
+ {
+ { "460", "中国" },
+ { "001", "美国" },
+ { "004", "阿富汗" },
+ { "208", "法国" },
+ { "234", "英国" },
+ { "262", "德国" },
+ { "310", "美国" },
+ { "440", "日本" },
+ { "450", "韩国" },
+ { "505", "澳大利亚" },
+ { "530", "新西兰" },
+ { "724", "巴西" }
+ };
+
+ ///
+ /// 中国移动网络代码(MNC)映射
+ ///
+ private static readonly Dictionary ChinaMncMap = new()
+ {
+ { "00", "中国移动" },
+ { "02", "中国移动" },
+ { "04", "中国移动" },
+ { "07", "中国移动" },
+ { "08", "中国移动" },
+ { "01", "中国联通" },
+ { "06", "中国联通" },
+ { "09", "中国联通" },
+ { "03", "中国电信" },
+ { "05", "中国电信" },
+ { "11", "中国电信" },
+ { "15", "中国广电" }
+ };
+
+ ///
+ /// Luhn算法校验码权重
+ ///
+ private static readonly int[] LuhnWeights = { 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2 };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证ICCID是否有效
+ ///
+ /// ICCID号
+ /// 是否有效
+ public static bool IsValid(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return false;
+ }
+
+ return ValidateLuhn(iccid!);
+ }
+
+ ///
+ /// 验证ICCID格式
+ ///
+ /// ICCID号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? iccid)
+ {
+ if (string.IsNullOrWhiteSpace(iccid))
+ {
+ return false;
+ }
+
+ string cleaned = iccid.Trim();
+ return ICCIDRegex.IsMatch(cleaned);
+ }
+
+ ///
+ /// 使用Luhn算法验证ICCID
+ ///
+ /// ICCID号
+ /// 是否通过Luhn校验
+ public static bool ValidateLuhn(string? iccid)
+ {
+ if (string.IsNullOrWhiteSpace(iccid))
+ {
+ return false;
+ }
+
+ string cleaned = iccid.Trim();
+ int sum = 0;
+ int length = cleaned.Length;
+
+ // 从右向左,第1位是校验位
+ for (int i = length - 2; i >= 0; i--)
+ {
+ if (!char.IsDigit(cleaned[i]))
+ {
+ return false;
+ }
+
+ int digit = cleaned[i] - '0';
+ int weightIndex = (length - 2 - i);
+ int multiplier = (weightIndex % 2 == 0) ? 2 : 1;
+
+ digit *= multiplier;
+ if (digit > 9)
+ {
+ digit -= 9;
+ }
+
+ sum += digit;
+ }
+
+ int checkDigit = (10 - (sum % 10)) % 10;
+ int actualCheck = cleaned[length - 1] - '0';
+
+ return checkDigit == actualCheck;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取移动国家代码(MCC,前3位)
+ ///
+ /// ICCID号
+ /// 移动国家代码
+ public static string? GetMCC(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ return iccid!.Substring(0, 3);
+ }
+
+ ///
+ /// 获取国家名称
+ ///
+ /// ICCID号
+ /// 国家名称
+ public static string? GetCountry(string? iccid)
+ {
+ string? mcc = GetMCC(iccid);
+ if (mcc == null)
+ {
+ return null;
+ }
+
+ return MccMap.TryGetValue(mcc, out string? country) ? country : null;
+ }
+
+ ///
+ /// 获取移动网络代码(MNC,第4-5位)
+ ///
+ /// ICCID号
+ /// 移动网络代码
+ public static string? GetMNC(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ return iccid!.Substring(3, 2);
+ }
+
+ ///
+ /// 获取运营商名称(仅支持中国运营商)
+ ///
+ /// ICCID号
+ /// 运营商名称
+ public static string? GetCarrier(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ string? mcc = GetMCC(iccid);
+ if (mcc != "460")
+ {
+ return null; // 非中国卡
+ }
+
+ string mnc = iccid!.Substring(3, 2);
+ return ChinaMncMap.TryGetValue(mnc, out string? carrier) ? carrier : null;
+ }
+
+ ///
+ /// 判断是否为中国移动
+ ///
+ public static bool IsChinaMobile(string? iccid) => GetCarrier(iccid) == "中国移动";
+
+ ///
+ /// 判断是否为中国联通
+ ///
+ public static bool IsChinaUnicom(string? iccid) => GetCarrier(iccid) == "中国联通";
+
+ ///
+ /// 判断是否为中国电信
+ ///
+ public static bool IsChinaTelecom(string? iccid) => GetCarrier(iccid) == "中国电信";
+
+ ///
+ /// 判断是否为中国广电
+ ///
+ public static bool IsChinaBroadnet(string? iccid) => GetCarrier(iccid) == "中国广电";
+
+ ///
+ /// 获取发卡省份代码(第9-10位)
+ ///
+ /// ICCID号
+ /// 省份代码
+ public static string? GetProvinceCode(string? iccid)
+ {
+ if (!IsValidFormat(iccid) || iccid!.Length < 10)
+ {
+ return null;
+ }
+
+ return iccid.Substring(8, 2);
+ }
+
+ ///
+ /// 获取序列号(第11-19位,不含校验位)
+ ///
+ /// ICCID号
+ /// 序列号
+ public static string? GetSerialNumber(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ int length = iccid!.Length;
+ return iccid.Substring(10, length - 11);
+ }
+
+ ///
+ /// 获取校验位(最后一位)
+ ///
+ /// ICCID号
+ /// 校验位
+ public static int? GetCheckDigit(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ return iccid![iccid.Length - 1] - '0';
+ }
+
+ ///
+ /// 解析ICCID结构
+ ///
+ /// ICCID号
+ /// ICCID结构信息
+ public static ICCDInfo? Parse(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ return new ICCDInfo
+ {
+ MCC = GetMCC(iccid),
+ Country = GetCountry(iccid),
+ MNC = GetMNC(iccid),
+ Carrier = GetCarrier(iccid),
+ ProvinceCode = GetProvinceCode(iccid),
+ SerialNumber = GetSerialNumber(iccid),
+ CheckDigit = GetCheckDigit(iccid)
+ };
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化ICCID(去除空格和分隔符)
+ ///
+ /// ICCID号
+ /// 格式化后的ICCID
+ public static string? Normalize(string? iccid)
+ {
+ if (string.IsNullOrWhiteSpace(iccid))
+ {
+ return null;
+ }
+
+ string cleaned = iccid.Trim();
+ return ICCIDRegex.IsMatch(cleaned) ? cleaned : null;
+ }
+
+ ///
+ /// 格式化为易读格式:898600 00 00 1234567890
+ ///
+ /// ICCID号
+ /// 格式化后的ICCID
+ public static string? Format(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ string cleaned = iccid!.Trim();
+ if (cleaned.Length == 19)
+ {
+ return $"{cleaned.Substring(0, 6)} {cleaned.Substring(6, 2)} {cleaned.Substring(8, 2)} {cleaned.Substring(10)}";
+ }
+ else if (cleaned.Length == 20)
+ {
+ return $"{cleaned.Substring(0, 6)} {cleaned.Substring(6, 2)} {cleaned.Substring(8, 3)} {cleaned.Substring(11)}";
+ }
+
+ return cleaned;
+ }
+
+ ///
+ /// ICCID脱敏:898600****7890
+ ///
+ /// ICCID号
+ /// 脱敏后的ICCID
+ public static string? Mask(string? iccid)
+ {
+ if (!IsValidFormat(iccid))
+ {
+ return null;
+ }
+
+ string cleaned = iccid!.Trim();
+ int length = cleaned.Length;
+
+ // 保留前6位和后4位
+ return cleaned.Substring(0, 6) + new string('*', length - 10) + cleaned.Substring(length - 4);
+ }
+
+ #endregion
+ }
+
+ ///
+ /// ICCID结构信息
+ ///
+ public class ICCDInfo
+ {
+ ///
+ /// 移动国家代码(MCC)
+ ///
+ public string? MCC { get; set; }
+
+ ///
+ /// 国家名称
+ ///
+ public string? Country { get; set; }
+
+ ///
+ /// 移动网络代码(MNC)
+ ///
+ public string? MNC { get; set; }
+
+ ///
+ /// 运营商名称
+ ///
+ public string? Carrier { get; set; }
+
+ ///
+ /// 省份代码
+ ///
+ public string? ProvinceCode { get; set; }
+
+ ///
+ /// 序列号
+ ///
+ public string? SerialNumber { get; set; }
+
+ ///
+ /// 校验位
+ ///
+ public int? CheckDigit { get; set; }
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/IMEIUtil.cs b/EasyTool.Core/BusinessCategory/IMEIUtil.cs
new file mode 100644
index 0000000..7d88853
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/IMEIUtil.cs
@@ -0,0 +1,347 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// IMEI(国际移动设备识别号)工具类
+ ///
+ public static class IMEIUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// IMEI正则表达式(15位数字)
+ ///
+ private static readonly Regex IMEIRegex = new(@"^\d{15}$", RegexOptions.Compiled);
+
+ ///
+ /// IMEI SV正则表达式(16位数字,含软件版本)
+ ///
+ private static readonly Regex IMEISvRegex = new(@"^\d{16}$", RegexOptions.Compiled);
+
+ ///
+ /// TAC(类型分配码)与制造商映射(部分)
+ ///
+ private static readonly (string Prefix, string Manufacturer)[] TacPrefixMap =
+ {
+ ("01", "Apple"),
+ ("35", "Samsung"),
+ ("86", "Samsung"),
+ ("01", "Nokia"),
+ ("35", "Nokia"),
+ ("352", "Sony"),
+ ("353", "Sony"),
+ ("354", "Sony"),
+ ("355", "Sony"),
+ ("356", "Sony"),
+ ("358", "Huawei"),
+ ("359", "Huawei"),
+ ("861", "Xiaomi"),
+ ("862", "Xiaomi"),
+ ("865", "Xiaomi"),
+ ("866", "Xiaomi"),
+ ("352", "LG"),
+ ("353", "LG"),
+ ("355", "LG"),
+ ("356", "LG"),
+ ("353", "HTC"),
+ ("354", "HTC"),
+ ("355", "HTC"),
+ ("357", "HTC"),
+ ("358", "HTC"),
+ ("359", "HTC"),
+ ("010", "Apple"),
+ ("011", "Apple"),
+ ("012", "Apple"),
+ ("013", "Apple"),
+ ("014", "Apple"),
+ ("015", "Apple"),
+ ("016", "Apple"),
+ ("017", "Apple"),
+ ("018", "Apple"),
+ ("019", "Apple")
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证IMEI是否有效(15位,含Luhn校验)
+ ///
+ /// IMEI号
+ /// 是否有效
+ public static bool IsValid(string? imei)
+ {
+ if (!IsValidFormat(imei))
+ {
+ return false;
+ }
+
+ return ValidateLuhn(imei!);
+ }
+
+ ///
+ /// 仅验证IMEI格式(不校验Luhn)
+ ///
+ /// IMEI号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? imei)
+ {
+ if (string.IsNullOrWhiteSpace(imei))
+ {
+ return false;
+ }
+
+ return IMEIRegex.IsMatch(imei);
+ }
+
+ ///
+ /// 验证IMEI SV是否有效(16位)
+ ///
+ /// IMEI SV号
+ /// 是否有效
+ public static bool IsValidSv(string? imeiSv)
+ {
+ if (string.IsNullOrWhiteSpace(imeiSv))
+ {
+ return false;
+ }
+
+ return IMEISvRegex.IsMatch(imeiSv);
+ }
+
+ ///
+ /// 使用Luhn算法验证IMEI
+ ///
+ /// IMEI号
+ /// 是否通过Luhn校验
+ public static bool ValidateLuhn(string? imei)
+ {
+ if (string.IsNullOrWhiteSpace(imei) || imei.Length != 15)
+ {
+ return false;
+ }
+
+ int sum = 0;
+ for (int i = 0; i < 15; i++)
+ {
+ if (!char.IsDigit(imei[i]))
+ {
+ return false;
+ }
+
+ int digit = imei[i] - '0';
+
+ // 偶数位置(从0开始)乘以2,奇数位置不变
+ // IMEI的Luhn算法:从右向左,偶数位×2
+ if (i % 2 == 1)
+ {
+ digit *= 2;
+ if (digit > 9)
+ {
+ digit -= 9;
+ }
+ }
+
+ sum += digit;
+ }
+
+ return sum % 10 == 0;
+ }
+
+ ///
+ /// 计算Luhn校验位
+ ///
+ /// 不含校验位的14位IMEI
+ /// 校验位(0-9),计算失败返回-1
+ public static int CalculateCheckDigit(string? imei14)
+ {
+ if (string.IsNullOrWhiteSpace(imei14) || imei14.Length != 14)
+ {
+ return -1;
+ }
+
+ int sum = 0;
+ for (int i = 0; i < 14; i++)
+ {
+ if (!char.IsDigit(imei14[i]))
+ {
+ return -1;
+ }
+
+ int digit = imei14[i] - '0';
+
+ if (i % 2 == 1)
+ {
+ digit *= 2;
+ if (digit > 9)
+ {
+ digit -= 9;
+ }
+ }
+
+ sum += digit;
+ }
+
+ return (10 - (sum % 10)) % 10;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取TAC(类型分配码,前8位)
+ ///
+ /// IMEI号
+ /// TAC码
+ public static string? GetTAC(string? imei)
+ {
+ if (!IsValidFormat(imei))
+ {
+ return null;
+ }
+
+ return imei!.Substring(0, 8);
+ }
+
+ ///
+ /// 获取制造商(根据TAC前缀推测)
+ ///
+ /// IMEI号
+ /// 制造商名称
+ public static string? GetManufacturer(string? imei)
+ {
+ if (!IsValidFormat(imei))
+ {
+ return null;
+ }
+
+ string tac = imei!.Substring(0, 8);
+
+ // 查找最长匹配的前缀
+ for (int len = Math.Min(3, tac.Length); len >= 1; len--)
+ {
+ string prefix = tac.Substring(0, len);
+ foreach (var mapping in TacPrefixMap)
+ {
+ if (mapping.Prefix == prefix)
+ {
+ return mapping.Manufacturer;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取序列号(SNR,第9-14位)
+ ///
+ /// IMEI号
+ /// 序列号
+ public static string? GetSerialNumber(string? imei)
+ {
+ if (!IsValidFormat(imei))
+ {
+ return null;
+ }
+
+ return imei!.Substring(8, 6);
+ }
+
+ ///
+ /// 获取校验位(第15位)
+ ///
+ /// IMEI号
+ /// 校验位
+ public static int? GetCheckDigit(string? imei)
+ {
+ if (!IsValidFormat(imei))
+ {
+ return null;
+ }
+
+ return imei![14] - '0';
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化IMEI(AA-BBBBBB-CCCCCC-D)
+ ///
+ /// IMEI号
+ /// 格式化后的IMEI
+ public static string? Format(string? imei)
+ {
+ string? normalized = Normalize(imei);
+ if (normalized == null || normalized.Length != 15)
+ {
+ return null;
+ }
+
+ return $"{normalized.Substring(0, 2)}-{normalized.Substring(2, 6)}-{normalized.Substring(8, 6)}-{normalized[14]}";
+ }
+
+ ///
+ /// 格式化IMEI(去除分隔符)
+ ///
+ /// IMEI号
+ /// 清理后的IMEI
+ public static string? Normalize(string? imei)
+ {
+ if (string.IsNullOrWhiteSpace(imei))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(imei, @"[^\d]", "");
+ return cleaned.Length == 15 ? cleaned : null;
+ }
+
+ ///
+ /// IMEI脱敏:35****6
+ ///
+ /// IMEI号
+ /// 脱敏后的IMEI
+ public static string? Mask(string? imei)
+ {
+ string? normalized = Normalize(imei);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ return normalized.Substring(0, 2) + "***********" + normalized[14];
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机IMEI(仅供测试使用)
+ ///
+ /// TAC码(可选,默认随机)
+ /// 15位IMEI
+ public static string GenerateRandom(string? tac = null)
+ {
+ // TAC(8位)
+ string tacCode = tac ?? MathCategory.RandomUtil.RandomDigitString(8);
+
+ // 序列号(6位)
+ string serial = MathCategory.RandomUtil.RandomDigitString(6);
+
+ // 计算校验位
+ int checkDigit = CalculateCheckDigit(tacCode + serial);
+
+ return tacCode + serial + checkDigit;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/IPv6Util.cs b/EasyTool.Core/BusinessCategory/IPv6Util.cs
new file mode 100644
index 0000000..945679e
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/IPv6Util.cs
@@ -0,0 +1,269 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// IPv6 地址工具类
+ /// 用于验证和处理 IPv6 地址
+ ///
+ public static class IPv6Util
+ {
+ ///
+ /// IPv6 正则表达式
+ ///
+ private static readonly Regex IPv6Regex = new(
+ @"^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 验证是否为有效的 IPv6 地址
+ ///
+ /// IP 地址
+ /// 是否有效
+ public static bool IsValid(string? address)
+ {
+ if (string.IsNullOrWhiteSpace(address))
+ return false;
+
+ return IPv6Regex.IsMatch(address.Trim());
+ }
+
+ ///
+ /// 压缩 IPv6 地址(移除前导零和连续零块)
+ ///
+ /// IPv6 地址
+ /// 压缩后的地址
+ public static string Compress(string address)
+ {
+ if (!IsValid(address))
+ throw new ArgumentException("无效的 IPv6 地址", nameof(address));
+
+ try
+ {
+ var ip = System.Net.IPAddress.Parse(address);
+ return ip.IsIPv6LinkLocal ? address : ip.ToString();
+ }
+ catch
+ {
+ return address;
+ }
+ }
+
+ ///
+ /// 展开 IPv6 地址(补全省略的零)
+ ///
+ /// IPv6 地址
+ /// 展开后的地址
+ public static string Expand(string address)
+ {
+ if (!IsValid(address))
+ throw new ArgumentException("无效的 IPv6 地址", nameof(address));
+
+ try
+ {
+ var ip = System.Net.IPAddress.Parse(address);
+ var bytes = ip.GetAddressBytes();
+
+ var result = new System.Text.StringBuilder();
+ for (int i = 0; i < 16; i += 2)
+ {
+ if (i > 0) result.Append(':');
+ result.Append($"{bytes[i]:x2}{bytes[i + 1]:x2}");
+ }
+
+ return result.ToString();
+ }
+ catch
+ {
+ return address;
+ }
+ }
+
+ ///
+ /// 判断是否为本地链接地址(fe80::/10)
+ ///
+ /// IPv6 地址
+ /// 是否为本地链接地址
+ public static bool IsLinkLocal(string? address)
+ {
+ if (!IsValid(address))
+ return false;
+
+ try
+ {
+ var ip = System.Net.IPAddress.Parse(address);
+ return ip.IsIPv6LinkLocal;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 判断是否为回环地址(::1)
+ ///
+ /// IPv6 地址
+ /// 是否为回环地址
+ public static bool IsLoopback(string? address)
+ {
+ if (!IsValid(address))
+ return false;
+
+ return System.Net.IPAddress.TryParse(address, out var ip) &&
+ System.Net.IPAddress.IsLoopback(ip);
+ }
+
+ ///
+ /// 判断是否为私有地址
+ /// fc00::/7 (Unique Local Address)
+ ///
+ /// IPv6 地址
+ /// 是否为私有地址
+ public static bool IsPrivate(string? address)
+ {
+ if (!IsValid(address))
+ return false;
+
+ var expanded = Expand(address).Replace(":", "").ToLower();
+ return expanded.StartsWith("fc") || expanded.StartsWith("fd");
+ }
+
+ ///
+ /// 判断是否为多播地址(ff00::/8)
+ ///
+ /// IPv6 地址
+ /// 是否为多播地址
+ public static bool IsMulticast(string? address)
+ {
+ if (!IsValid(address))
+ return false;
+
+ return address!.TrimStart().StartsWith("ff", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// IPv4 映射的 IPv6 地址转换为 IPv4
+ ///
+ /// IPv6 地址(::ffff:192.168.1.1 格式)
+ /// IPv4 地址
+ public static string? ToIPv4(string? address)
+ {
+ if (!IsValid(address))
+ return null;
+
+ try
+ {
+ var ip = System.Net.IPAddress.Parse(address);
+ if (ip.IsIPv4MappedToIPv6)
+ {
+ return ip.MapToIPv4().ToString();
+ }
+ return null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// IPv4 转换为 IPv6 映射地址
+ ///
+ /// IPv4 地址
+ /// IPv6 映射地址
+ public static string? FromIPv4(string? ipv4Address)
+ {
+ if (string.IsNullOrWhiteSpace(ipv4Address))
+ return null;
+
+ try
+ {
+ var ip = System.Net.IPAddress.Parse(ipv4Address);
+ if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
+ {
+ return ip.MapToIPv6().ToString();
+ }
+ return null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 获取 IPv6 地址类型
+ ///
+ /// IPv6 地址
+ /// 地址类型
+ public static IPv6AddressType GetAddressType(string? address)
+ {
+ if (!IsValid(address))
+ return IPv6AddressType.Unknown;
+
+ if (IsLoopback(address))
+ return IPv6AddressType.Loopback;
+
+ if (IsLinkLocal(address))
+ return IPv6AddressType.LinkLocal;
+
+ if (IsPrivate(address))
+ return IPv6AddressType.UniqueLocal;
+
+ if (IsMulticast(address))
+ return IPv6AddressType.Multicast;
+
+ if (address!.StartsWith("2", StringComparison.OrdinalIgnoreCase) ||
+ address.StartsWith("3", StringComparison.OrdinalIgnoreCase))
+ return IPv6AddressType.GlobalUnicast;
+
+ if (address.StartsWith("::", StringComparison.Ordinal))
+ return IPv6AddressType.Unspecified;
+
+ return IPv6AddressType.GlobalUnicast;
+ }
+ }
+
+ ///
+ /// IPv6 地址类型
+ ///
+ public enum IPv6AddressType
+ {
+ ///
+ /// 未知
+ ///
+ Unknown,
+
+ ///
+ /// 未指定地址(::)
+ ///
+ Unspecified,
+
+ ///
+ /// 回环地址(::1)
+ ///
+ Loopback,
+
+ ///
+ /// 本地链接地址(fe80::/10)
+ ///
+ LinkLocal,
+
+ ///
+ /// 唯一本地地址(fc00::/7)
+ ///
+ UniqueLocal,
+
+ ///
+ /// 全球单播地址
+ ///
+ GlobalUnicast,
+
+ ///
+ /// 多播地址(ff00::/8)
+ ///
+ Multicast
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/ISBNUtil.cs b/EasyTool.Core/BusinessCategory/ISBNUtil.cs
new file mode 100644
index 0000000..4ac33c8
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/ISBNUtil.cs
@@ -0,0 +1,576 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// ISBN类型枚举
+ ///
+ public enum ISBNType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// ISBN-10(10位)
+ ///
+ ISBN10 = 1,
+
+ ///
+ /// ISBN-13(13位)
+ ///
+ ISBN13 = 2
+ }
+
+ ///
+ /// ISBN书号工具类
+ ///
+ public static class ISBNUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// ISBN-10正则表达式(可含分隔符)
+ ///
+ private static readonly Regex ISBN10Regex = new Regex(
+ @"^(\d{1,5}[-\s]?)?\d{1,7}[-\s]?\d{1,7}[-\s]?[\dXx]$",
+ RegexOptions.Compiled);
+
+ ///
+ /// ISBN-13正则表达式(可含分隔符)
+ ///
+ private static readonly Regex ISBN13Regex = new Regex(
+ @"^97[89][-\s]?\d{1,5}[-\s]?\d{1,7}[-\s]?\d{1,7}[-\s]?\d$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 纯数字ISBN-10正则
+ ///
+ private static readonly Regex ISBN10CleanRegex = new Regex(
+ @"^\d{9}[\dXx]$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 纯数字ISBN-13正则
+ ///
+ private static readonly Regex ISBN13CleanRegex = new Regex(
+ @"^97[89]\d{10}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// ISBN前缀与国家/地区/语言映射
+ ///
+ private static readonly (string Prefix, string Region)[] PrefixRegionMap =
+ {
+ ("0", "英语国家"), ("1", "英语国家"),
+ ("2", "法语国家"),
+ ("3", "德语国家"),
+ ("4", "日本"),
+ ("5", "前苏联/俄罗斯"),
+ ("7", "中国"),
+ ("80", "前捷克斯洛伐克"), ("85", "巴西"),
+ ("87", "丹麦"),
+ ("88", "意大利"),
+ ("90", "荷兰"), ("91", "瑞典"), ("92", "国际组织"),
+ ("93", "印度"), ("94", "荷兰"),
+ ("952", "芬兰"), ("953", "克罗地亚"),
+ ("960", "希腊"), ("961", "斯洛文尼亚"), ("962", "香港"),
+ ("963", "匈牙利"), ("964", "伊朗"), ("965", "以色列"),
+ ("966", "乌克兰"), ("967", "马来西亚"), ("968", "墨西哥"),
+ ("969", "巴基斯坦"), ("970", "墨西哥"), ("971", "菲律宾"),
+ ("972", "葡萄牙"), ("973", "罗马尼亚"), ("974", "泰国"),
+ ("975", "土耳其"), ("976", "加勒比海地区"), ("977", "埃及"),
+ ("978", "尼日利亚"), ("979", "印度尼西亚"),
+ ("980", "委内瑞拉"), ("981", "新加坡"), ("982", "南太平洋地区"),
+ ("983", "马来西亚"), ("984", "孟加拉"), ("985", "白俄罗斯"),
+ ("986", "台湾"), ("987", "阿根廷"), ("988", "香港"),
+ ("989", "葡萄牙"), ("9927", "沙特阿拉伯"), ("9933", "伊朗"),
+ ("9937", "尼泊尔"), ("9939", "亚美尼亚"), ("9940", "卡塔尔"),
+ ("9942", "阿塞拜疆"), ("9943", "塔吉克斯坦"), ("9944", "斯洛伐克"),
+ ("9945", "朝鲜"), ("9946", "阿尔巴尼亚"), ("9947", "阿联酋"),
+ ("9948", "黎巴嫩"), ("9949", "爱沙尼亚"), ("9950", "叙利亚"),
+ ("9951", "约旦"), ("9952", "吉尔吉斯斯坦"), ("9953", "巴勒斯坦"),
+ ("9954", "摩洛哥"), ("9955", "立陶宛"), ("9956", "喀麦隆"),
+ ("9957", "约旦"), ("9958", "古巴"), ("9959", "阿尔及利亚"),
+ ("9960", "沙特阿拉伯"), ("9961", "阿曼"), ("9962", "巴林"),
+ ("9963", "冰岛"), ("9964", "加纳"), ("9965", "科威特"),
+ ("9966", "肯尼亚"), ("9967", "吉布提"), ("9968", "厄瓜多尔"),
+ ("9969", "蒙古"), ("9970", "乌干达"), ("9971", "津巴布韦"),
+ ("9972", "巴拿马"), ("9973", "突尼斯"), ("9974", "塞内加尔"),
+ ("9975", "罗马尼亚"), ("9976", "巴布亚新几内亚"), ("9977", "哥斯达黎加"),
+ ("9978", "斯里兰卡"), ("9979", "冰岛"), ("9980", "刚果"),
+ ("9981", "马达加斯加"), ("9982", "加蓬"), ("9983", "马里"),
+ ("9984", "马拉维"), ("9985", "爱沙尼亚"), ("9986", "立陶宛"),
+ ("9987", "坦桑尼亚"), ("9988", "加纳"), ("9989", "马其顿"),
+ ("99901", "巴哈马"), ("99903", "莫桑比克"), ("99904", "哈萨克斯坦"),
+ ("99905", "尼泊尔"), ("99906", "马拉维"), ("99908", "澳门")
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证ISBN是否有效(自动识别ISBN-10或ISBN-13)
+ ///
+ /// ISBN号
+ /// 是否有效
+ public static bool IsValid(string? isbn)
+ {
+ if (string.IsNullOrWhiteSpace(isbn))
+ {
+ return false;
+ }
+
+ string cleaned = CleanISBN(isbn);
+
+ if (cleaned.Length == 10)
+ {
+ return IsValidISBN10(cleaned);
+ }
+
+ if (cleaned.Length == 13)
+ {
+ return IsValidISBN13(cleaned);
+ }
+
+ return false;
+ }
+
+ ///
+ /// 验证ISBN-10是否有效
+ ///
+ /// ISBN号
+ /// 是否有效
+ public static bool IsValidISBN10(string? isbn)
+ {
+ if (string.IsNullOrWhiteSpace(isbn))
+ {
+ return false;
+ }
+
+ string cleaned = CleanISBN(isbn);
+
+ if (!ISBN10CleanRegex.IsMatch(cleaned))
+ {
+ return false;
+ }
+
+ // 计算校验位
+ int sum = 0;
+ for (int i = 0; i < 9; i++)
+ {
+ sum += (cleaned[i] - '0') * (10 - i);
+ }
+
+ // 最后一位可能是X(代表10)
+ char lastChar = char.ToUpper(cleaned[9]);
+ int checkDigit = lastChar == 'X' ? 10 : (lastChar - '0');
+ sum += checkDigit;
+
+ return sum % 11 == 0;
+ }
+
+ ///
+ /// 验证ISBN-13是否有效
+ ///
+ /// ISBN号
+ /// 是否有效
+ public static bool IsValidISBN13(string? isbn)
+ {
+ if (string.IsNullOrWhiteSpace(isbn))
+ {
+ return false;
+ }
+
+ string cleaned = CleanISBN(isbn);
+
+ if (!ISBN13CleanRegex.IsMatch(cleaned))
+ {
+ return false;
+ }
+
+ // ISBN-13必须以978或979开头
+ if (!cleaned.StartsWith("978") && !cleaned.StartsWith("979"))
+ {
+ return false;
+ }
+
+ // 计算校验位
+ int sum = 0;
+ for (int i = 0; i < 12; i++)
+ {
+ int digit = cleaned[i] - '0';
+ sum += digit * (i % 2 == 0 ? 1 : 3);
+ }
+
+ int checkDigit = (10 - (sum % 10)) % 10;
+ return checkDigit == (cleaned[12] - '0');
+ }
+
+ ///
+ /// 验证ISBN格式(不计算校验位)
+ ///
+ /// ISBN号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? isbn)
+ {
+ if (string.IsNullOrWhiteSpace(isbn))
+ {
+ return false;
+ }
+
+ string cleaned = CleanISBN(isbn);
+ return ISBN10CleanRegex.IsMatch(cleaned) || ISBN13CleanRegex.IsMatch(cleaned);
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取ISBN类型
+ ///
+ /// ISBN号
+ /// ISBN类型
+ public static ISBNType GetISBNType(string? isbn)
+ {
+ if (!IsValid(isbn))
+ {
+ return ISBNType.Unknown;
+ }
+
+ string cleaned = CleanISBN(isbn);
+ return cleaned.Length == 10 ? ISBNType.ISBN10 : ISBNType.ISBN13;
+ }
+
+ #endregion
+
+ #region 转换方法
+
+ ///
+ /// 将ISBN-10转换为ISBN-13
+ ///
+ /// ISBN-10号
+ /// ISBN-13号,转换失败返回null
+ public static string? ConvertToISBN13(string? isbn10)
+ {
+ if (!IsValidISBN10(isbn10))
+ {
+ return null;
+ }
+
+ string cleaned = CleanISBN(isbn10!);
+
+ // 添加前缀978
+ string isbn13 = "978" + cleaned.Substring(0, 9);
+
+ // 计算新的校验位
+ int sum = 0;
+ for (int i = 0; i < 12; i++)
+ {
+ int digit = isbn13[i] - '0';
+ sum += digit * (i % 2 == 0 ? 1 : 3);
+ }
+
+ int checkDigit = (10 - (sum % 10)) % 10;
+ return isbn13 + checkDigit;
+ }
+
+ ///
+ /// 将ISBN-13转换为ISBN-10(仅适用于978前缀)
+ ///
+ /// ISBN-13号
+ /// ISBN-10号,转换失败返回null
+ public static string? ConvertToISBN10(string? isbn13)
+ {
+ if (!IsValidISBN13(isbn13))
+ {
+ return null;
+ }
+
+ string cleaned = CleanISBN(isbn13!);
+
+ // 只有978前缀才能转换为ISBN-10
+ if (!cleaned.StartsWith("978"))
+ {
+ return null;
+ }
+
+ // 去掉前缀978和最后一位校验位
+ string isbn10Body = cleaned.Substring(3, 9);
+
+ // 计算ISBN-10校验位
+ int sum = 0;
+ for (int i = 0; i < 9; i++)
+ {
+ sum += (isbn10Body[i] - '0') * (10 - i);
+ }
+
+ int checkValue = 11 - (sum % 11);
+ char checkChar;
+ if (checkValue == 10)
+ {
+ checkChar = 'X';
+ }
+ else if (checkValue == 11)
+ {
+ checkChar = '0';
+ }
+ else
+ {
+ checkChar = (char)('0' + checkValue);
+ }
+
+ return isbn10Body + checkChar;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取国家/地区名称
+ ///
+ /// ISBN号
+ /// 国家/地区名称
+ public static string? GetRegion(string? isbn)
+ {
+ if (!IsValid(isbn))
+ {
+ return null;
+ }
+
+ string cleaned = CleanISBN(isbn!);
+
+ // ISBN-13需要去掉978/979前缀
+ string prefix = cleaned.Length == 13 ? cleaned.Substring(3) : cleaned;
+
+ // 查找最长匹配的前缀
+ for (int len = Math.Min(5, prefix.Length); len >= 1; len--)
+ {
+ string searchPrefix = prefix.Substring(0, len);
+ foreach (var mapping in PrefixRegionMap)
+ {
+ if (mapping.Prefix == searchPrefix)
+ {
+ return mapping.Region;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 判断是否为中国出版物
+ ///
+ /// ISBN号
+ /// 是否为中国出版物
+ public static bool IsChinaISBN(string? isbn)
+ {
+ if (!IsValid(isbn))
+ {
+ return false;
+ }
+
+ string cleaned = CleanISBN(isbn!);
+
+ // ISBN-13: 978-7 或 979-7
+ // ISBN-10: 7开头
+ if (cleaned.Length == 13)
+ {
+ return cleaned.StartsWith("9787") || cleaned.StartsWith("9797");
+ }
+ else
+ {
+ return cleaned.StartsWith("7");
+ }
+ }
+
+ ///
+ /// 计算ISBN-10校验位
+ ///
+ /// 不含校验位的9位数字
+ /// 校验位(0-10,10表示X),计算失败返回-1
+ public static int CalculateISBN10CheckDigit(string? isbn9)
+ {
+ if (string.IsNullOrWhiteSpace(isbn9) || isbn9.Length != 9)
+ {
+ return -1;
+ }
+
+ int sum = 0;
+ for (int i = 0; i < 9; i++)
+ {
+ if (!char.IsDigit(isbn9[i]))
+ {
+ return -1;
+ }
+ sum += (isbn9[i] - '0') * (10 - i);
+ }
+
+ int checkValue = 11 - (sum % 11);
+ return checkValue == 11 ? 0 : checkValue;
+ }
+
+ ///
+ /// 计算ISBN-13校验位
+ ///
+ /// 不含校验位的12位数字
+ /// 校验位(0-9),计算失败返回-1
+ public static int CalculateISBN13CheckDigit(string? isbn12)
+ {
+ if (string.IsNullOrWhiteSpace(isbn12) || isbn12.Length != 12)
+ {
+ return -1;
+ }
+
+ int sum = 0;
+ for (int i = 0; i < 12; i++)
+ {
+ if (!char.IsDigit(isbn12[i]))
+ {
+ return -1;
+ }
+ int digit = isbn12[i] - '0';
+ sum += digit * (i % 2 == 0 ? 1 : 3);
+ }
+
+ return (10 - (sum % 10)) % 10;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 清理ISBN(去除分隔符)
+ ///
+ /// ISBN号
+ /// 清理后的ISBN
+ public static string CleanISBN(string? isbn)
+ {
+ if (string.IsNullOrWhiteSpace(isbn))
+ {
+ return "";
+ }
+
+ // 去除空格和横线
+ return Regex.Replace(isbn, @"[\s\-]", "").ToUpper();
+ }
+
+ ///
+ /// 格式化ISBN(添加分隔符)
+ ///
+ /// ISBN号
+ /// 格式化后的ISBN,如978-7-115-12345-6
+ public static string? Format(string? isbn)
+ {
+ if (!IsValid(isbn))
+ {
+ return null;
+ }
+
+ string cleaned = CleanISBN(isbn!);
+
+ if (cleaned.Length == 10)
+ {
+ // ISBN-10格式:x-x-xxx-xxxxx-x
+ return $"{cleaned[0]}-{cleaned[1]}-{cleaned.Substring(2, 3)}-{cleaned.Substring(5, 4)}-{cleaned[9]}";
+ }
+ else
+ {
+ // ISBN-13格式:xxx-x-xxx-xxxxx-x
+ return $"{cleaned.Substring(0, 3)}-{cleaned[3]}-{cleaned.Substring(4, 3)}-{cleaned.Substring(7, 5)}-{cleaned[12]}";
+ }
+ }
+
+ ///
+ /// 格式化ISBN(使用自定义分隔符)
+ ///
+ /// ISBN号
+ /// 分隔符
+ /// 格式化后的ISBN
+ public static string? Format(string? isbn, char separator)
+ {
+ string? formatted = Format(isbn);
+ if (formatted == null)
+ {
+ return null;
+ }
+
+ return formatted.Replace('-', separator);
+ }
+
+ ///
+ /// ISBN脱敏:978-7-***-*****-*
+ ///
+ /// ISBN号
+ /// 脱敏后的ISBN
+ public static string? Mask(string? isbn)
+ {
+ if (!IsValid(isbn))
+ {
+ return null;
+ }
+
+ string cleaned = CleanISBN(isbn!);
+
+ if (cleaned.Length == 10)
+ {
+ // 保留第1位和最后1位
+ return cleaned[0] + "*******" + cleaned[9];
+ }
+ else
+ {
+ // 保留前4位和最后1位
+ return cleaned.Substring(0, 4) + "*******" + cleaned[12];
+ }
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机ISBN-13(仅供测试使用)
+ ///
+ /// 前缀(默认978)
+ /// ISBN-13号
+ public static string GenerateRandomISBN13(string prefix = "978")
+ {
+ // 生成12位数字
+ string isbn12 = prefix + MathCategory.RandomUtil.RandomDigitString(12 - prefix.Length);
+
+ // 计算校验位
+ int checkDigit = CalculateISBN13CheckDigit(isbn12);
+
+ return isbn12 + checkDigit;
+ }
+
+ ///
+ /// 生成随机ISBN-10(仅供测试使用)
+ ///
+ /// ISBN-10号
+ public static string GenerateRandomISBN10()
+ {
+ // 生成9位数字
+ string isbn9 = MathCategory.RandomUtil.RandomDigitString(9);
+
+ // 计算校验位
+ int checkDigit = CalculateISBN10CheckDigit(isbn9);
+
+ if (checkDigit == 10)
+ {
+ return isbn9 + "X";
+ }
+
+ return isbn9 + checkDigit;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/IdCardUtil.cs b/EasyTool.Core/BusinessCategory/IdCardUtil.cs
new file mode 100644
index 0000000..d76621a
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/IdCardUtil.cs
@@ -0,0 +1,467 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 身份证工具类
+ ///
+ public static class IdCardUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 18位身份证校验码权重
+ ///
+ private static readonly int[] Weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
+
+ ///
+ /// 18位身份证校验码对照表
+ ///
+ private static readonly char[] CheckCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };
+
+ ///
+ /// 18位身份证正则表达式
+ ///
+ private static readonly Regex Regex18 = new Regex(@"^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$", RegexOptions.Compiled);
+
+ ///
+ /// 15位身份证正则表达式
+ ///
+ private static readonly Regex Regex15 = new Regex(@"^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$", RegexOptions.Compiled);
+
+ ///
+ /// 省份代码与名称映射
+ ///
+ private static readonly string[] ProvinceCodes = {
+ "", "北京", "天津", "河北", "山西", "内蒙古", // 11-15
+ "", "辽宁", "吉林", "黑龙江", "", // 21-23
+ "", "上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", // 31-37
+ "", "河南", "湖北", "湖南", "广东", "广西", "海南", // 41-46
+ "", "重庆", "四川", "贵州", "云南", "西藏", // 50-54
+ "", "陕西", "甘肃", "青海", "宁夏", "新疆", // 61-65
+ "", "台湾", // 71
+ "", "香港", "澳门" // 81-82
+ };
+
+ ///
+ /// 星座日期范围
+ ///
+ private static readonly (int Month, int Day, string Name)[] ZodiacRanges = {
+ (1, 20, "水瓶座"), (2, 19, "双鱼座"), (3, 21, "白羊座"),
+ (4, 20, "金牛座"), (5, 21, "双子座"), (6, 22, "巨蟹座"),
+ (7, 23, "狮子座"), (8, 23, "处女座"), (9, 23, "天秤座"),
+ (10, 24, "天蝎座"), (11, 23, "射手座"), (12, 22, "摩羯座")
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证身份证号是否有效(支持15位和18位)
+ ///
+ /// 身份证号
+ /// 是否有效
+ public static bool IsValid(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ return idCard.Length == 18 ? IsValid18(idCard) :
+ idCard.Length == 15 ? IsValid15(idCard) :
+ false;
+ }
+
+ ///
+ /// 验证18位身份证号是否有效
+ ///
+ /// 18位身份证号
+ /// 是否有效
+ public static bool IsValid18(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard) || idCard.Length != 18)
+ {
+ return false;
+ }
+
+ if (!Regex18.IsMatch(idCard))
+ {
+ return false;
+ }
+
+ // 验证日期有效性
+ if (!IsValidDate(idCard.Substring(6, 8)))
+ {
+ return false;
+ }
+
+ // 验证校验码
+ int sum = 0;
+ for (int i = 0; i < 17; i++)
+ {
+ sum += (idCard[i] - '0') * Weights[i];
+ }
+
+ char expectedCheckCode = CheckCodes[sum % 11];
+ char actualCheckCode = char.ToUpper(idCard[17]);
+
+ return expectedCheckCode == actualCheckCode;
+ }
+
+ ///
+ /// 验证15位身份证号是否有效
+ ///
+ /// 15位身份证号
+ /// 是否有效
+ public static bool IsValid15(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard) || idCard.Length != 15)
+ {
+ return false;
+ }
+
+ if (!Regex15.IsMatch(idCard))
+ {
+ return false;
+ }
+
+ // 验证日期有效性(15位身份证年份默认为19xx)
+ string dateStr = "19" + idCard.Substring(6, 6);
+ return IsValidDate(dateStr);
+ }
+
+ #endregion
+
+ #region 转换方法
+
+ ///
+ /// 将15位身份证号转换为18位
+ ///
+ /// 15位身份证号
+ /// 18位身份证号,转换失败返回null
+ public static string? Convert15To18(string? idCard15)
+ {
+ if (!IsValid15(idCard15))
+ {
+ return null;
+ }
+
+ // 在第6位后插入"19"
+ string idCard17 = idCard15!.Substring(0, 6) + "19" + idCard15.Substring(6);
+
+ // 计算校验码
+ int sum = 0;
+ for (int i = 0; i < 17; i++)
+ {
+ sum += (idCard17[i] - '0') * Weights[i];
+ }
+
+ return idCard17 + CheckCodes[sum % 11];
+ }
+
+ ///
+ /// 将18位身份证号转换为15位
+ ///
+ /// 18位身份证号
+ /// 15位身份证号,转换失败返回null
+ public static string? Convert18To15(string? idCard18)
+ {
+ if (!IsValid18(idCard18))
+ {
+ return null;
+ }
+
+ // 移除第6-9位的年份前两位"19"和最后一位校验码
+ return idCard18!.Substring(0, 6) + idCard18.Substring(8, 9);
+ }
+
+ #endregion
+
+ #region 信息提取方法
+
+ ///
+ /// 获取出生日期
+ ///
+ /// 身份证号
+ /// 出生日期,解析失败返回null
+ public static DateTime? GetBirthday(string? idCard)
+ {
+ if (!IsValid(idCard))
+ {
+ return null;
+ }
+
+ string dateStr;
+ if (idCard!.Length == 18)
+ {
+ dateStr = idCard.Substring(6, 8);
+ }
+ else
+ {
+ dateStr = "19" + idCard.Substring(6, 6);
+ }
+
+ int year = int.Parse(dateStr.Substring(0, 4));
+ int month = int.Parse(dateStr.Substring(4, 2));
+ int day = int.Parse(dateStr.Substring(6, 2));
+
+ return new DateTime(year, month, day);
+ }
+
+ ///
+ /// 获取年龄
+ ///
+ /// 身份证号
+ /// 年龄,解析失败返回null
+ public static int? GetAge(string? idCard)
+ {
+ DateTime? birthday = GetBirthday(idCard);
+ if (!birthday.HasValue)
+ {
+ return null;
+ }
+
+ DateTime today = DateTime.Today;
+ int age = today.Year - birthday.Value.Year;
+
+ // 如果今年生日还没过,年龄减1
+ if (today < birthday.Value.AddYears(age))
+ {
+ age--;
+ }
+
+ return age;
+ }
+
+ ///
+ /// 获取性别代码(1男2女)
+ ///
+ /// 身份证号
+ /// 性别代码,解析失败返回null
+ public static int? GetGender(string? idCard)
+ {
+ if (!IsValid(idCard))
+ {
+ return null;
+ }
+
+ // 第17位(索引16)表示性别,奇数为男,偶数为女
+ int genderDigit;
+ if (idCard!.Length == 18)
+ {
+ genderDigit = idCard[16] - '0';
+ }
+ else
+ {
+ genderDigit = idCard[14] - '0';
+ }
+
+ return genderDigit % 2 == 1 ? 1 : 2;
+ }
+
+ ///
+ /// 获取性别字符串(男/女)
+ ///
+ /// 身份证号
+ /// 性别字符串,解析失败返回null
+ public static string? GetGenderString(string? idCard)
+ {
+ int? gender = GetGender(idCard);
+ if (!gender.HasValue)
+ {
+ return null;
+ }
+
+ return gender.Value == 1 ? "男" : "女";
+ }
+
+ ///
+ /// 获取省份名称
+ ///
+ /// 身份证号
+ /// 省份名称,解析失败返回null
+ public static string? GetProvince(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard) || idCard.Length < 2)
+ {
+ return null;
+ }
+
+ int provinceCode;
+ if (!int.TryParse(idCard.Substring(0, 2), out provinceCode))
+ {
+ return null;
+ }
+
+ if (provinceCode < 0 || provinceCode >= ProvinceCodes.Length)
+ {
+ return null;
+ }
+
+ return ProvinceCodes[provinceCode];
+ }
+
+ ///
+ /// 获取行政区划代码(前6位)
+ ///
+ /// 身份证号
+ /// 行政区划代码,解析失败返回null
+ public static string? GetAreaCode(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard) || (idCard.Length != 15 && idCard.Length != 18))
+ {
+ return null;
+ }
+
+ return idCard.Substring(0, 6);
+ }
+
+ ///
+ /// 获取生肖
+ ///
+ /// 身份证号
+ /// 生肖,解析失败返回null
+ public static string? GetChineseZodiac(string? idCard)
+ {
+ DateTime? birthday = GetBirthday(idCard);
+ if (!birthday.HasValue)
+ {
+ return null;
+ }
+
+ return EasyTool.DateTimeCategory.LunarCalendarUtil.GetChineseZodiac(birthday.Value);
+ }
+
+ ///
+ /// 获取星座
+ ///
+ /// 身份证号
+ /// 星座,解析失败返回null
+ public static string? GetZodiac(string? idCard)
+ {
+ DateTime? birthday = GetBirthday(idCard);
+ if (!birthday.HasValue)
+ {
+ return null;
+ }
+
+ return GetZodiacByDate(birthday.Value.Month, birthday.Value.Day);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机身份证号(仅供测试使用)
+ ///
+ /// 省份代码(可选,默认随机)
+ /// 出生日期(可选,默认随机)
+ /// 性别(可选,1男2女,默认随机)
+ /// 18位身份证号
+ public static string GenerateRandom(string? provinceCode = null, DateTime? birthday = null, int? gender = null)
+ {
+ // 省份代码
+ string province = provinceCode ?? GetRandomProvinceCode();
+
+ // 出生日期
+ DateTime birth = birthday ?? EasyTool.MathCategory.RandomUtil.GetRandomDateTime(
+ new DateTime(1950, 1, 1),
+ new DateTime(2005, 12, 31));
+ string birthStr = birth.ToString("yyyyMMdd");
+
+ // 顺序码(3位)+ 性别
+ string sequence = EasyTool.MathCategory.RandomUtil.RandomDigitString(2);
+ int genderDigit;
+ if (gender.HasValue && (gender.Value == 1 || gender.Value == 2))
+ {
+ // 指定性别的奇偶性
+ int randomDigit = EasyTool.MathCategory.RandomUtil.RandomInt(0, 4);
+ genderDigit = gender.Value == 1 ? randomDigit * 2 + 1 : randomDigit * 2;
+ }
+ else
+ {
+ genderDigit = EasyTool.MathCategory.RandomUtil.RandomInt(0, 9);
+ }
+ sequence += genderDigit.ToString();
+
+ // 前17位
+ string idCard17 = province + birthStr + sequence;
+
+ // 计算校验码
+ int sum = 0;
+ for (int i = 0; i < 17; i++)
+ {
+ sum += (idCard17[i] - '0') * Weights[i];
+ }
+
+ return idCard17 + CheckCodes[sum % 11];
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 验证日期字符串是否有效
+ ///
+ private static bool IsValidDate(string dateStr)
+ {
+ if (dateStr.Length != 8)
+ {
+ return false;
+ }
+
+ int year = int.Parse(dateStr.Substring(0, 4));
+ int month = int.Parse(dateStr.Substring(4, 2));
+ int day = int.Parse(dateStr.Substring(6, 2));
+
+ if (year < 1900 || year > 2100)
+ {
+ return false;
+ }
+
+ if (month < 1 || month > 12)
+ {
+ return false;
+ }
+
+ int maxDay = DateTime.DaysInMonth(year, month);
+ return day >= 1 && day <= maxDay;
+ }
+
+ ///
+ /// 根据日期获取星座
+ ///
+ private static string GetZodiacByDate(int month, int day)
+ {
+ // 星座按日期划分,摩羯座的特殊处理(跨年)
+ for (int i = ZodiacRanges.Length - 1; i >= 0; i--)
+ {
+ var zodiac = ZodiacRanges[i];
+ if (month > zodiac.Month || (month == zodiac.Month && day >= zodiac.Day))
+ {
+ return zodiac.Name;
+ }
+ }
+
+ // 1月1日到1月19日是摩羯座
+ return "摩羯座";
+ }
+
+ ///
+ /// 获取随机省份代码
+ ///
+ private static string GetRandomProvinceCode()
+ {
+ int[] validCodes = { 11, 12, 13, 14, 15, 21, 22, 23, 31, 32, 33, 34, 35, 36, 37, 41, 42, 43, 44, 45, 46, 50, 51, 52, 53, 54, 61, 62, 63, 64, 65 };
+ int code = EasyTool.MathCategory.RandomUtil.GetRandomElement(validCodes);
+ return code.ToString("00");
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/LicensePlateUtil.cs b/EasyTool.Core/BusinessCategory/LicensePlateUtil.cs
new file mode 100644
index 0000000..e303422
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/LicensePlateUtil.cs
@@ -0,0 +1,717 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 车牌类型枚举
+ ///
+ public enum PlateType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// 普通车牌/燃油车牌(7位)
+ ///
+ Normal = 1,
+
+ ///
+ /// 小型新能源车牌(8位,渐变绿色)
+ ///
+ NewEnergySmall = 2,
+
+ ///
+ /// 大型新能源车牌(8位,黄绿双色)
+ ///
+ NewEnergyLarge = 3,
+
+ ///
+ /// 武警车牌
+ ///
+ WJ = 4,
+
+ ///
+ /// 军队车牌
+ ///
+ Military = 5
+ }
+
+ ///
+ /// 新能源汽车类型枚举
+ ///
+ public enum NewEnergyType
+ {
+ ///
+ /// 纯电动汽车
+ ///
+ PureElectric = 0,
+
+ ///
+ /// 插电式混合动力汽车(含增程式)
+ ///
+ PluginHybrid = 1
+ }
+
+ ///
+ /// 车辆燃料类型枚举
+ ///
+ public enum FuelType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// 燃油车(汽油/柴油)
+ ///
+ Fuel = 1,
+
+ ///
+ /// 纯电动汽车
+ ///
+ PureElectric = 2,
+
+ ///
+ /// 插电式混合动力汽车(含增程式)
+ ///
+ PluginHybrid = 3
+ }
+
+ ///
+ /// 车牌号工具类
+ ///
+ public static class LicensePlateUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 普通车牌正则表达式(7位)
+ /// 格式:省份简称(1位汉字)+ 发牌机关代号(1位字母)+ 序号(5位字母或数字)
+ ///
+ private static readonly Regex NormalPlateRegex = new Regex(
+ @"^[京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新渝港澳台][A-Z][A-HJ-NP-Z0-9]{5}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 小型新能源车牌正则表达式(8位)
+ /// 格式:省份简称 + 字母 + 5位(第3位为D或F)
+ ///
+ private static readonly Regex NewEnergySmallRegex = new Regex(
+ @"^[京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新渝港澳台][A-Z][DF][A-HJ-NP-Z0-9]{5}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 大型新能源车牌正则表达式(8位)
+ /// 格式:省份简称 + 字母 + 5位(第3位或第4-8位包含数字,第8位为D或F)
+ ///
+ private static readonly Regex NewEnergyLargeRegex = new Regex(
+ @"^[京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新渝港澳台][A-Z][A-HJ-NP-Z0-9]{5}[DF]$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 武警车牌正则表达式
+ /// 格式:WJ + 省份代码(2位数字)+ 1位字母 + 4位数字
+ ///
+ private static readonly Regex WJPlateRegex = new Regex(
+ @"^WJ[0-9]{2}[0-9A-HJ-NP-Z]\d{4}$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 军队车牌正则表达式(简化版)
+ ///
+ private static readonly Regex MilitaryPlateRegex = new Regex(
+ @"^[VQZHBSLJKWETCYM][A-Z][A-HJ-NP-Z0-9]{5}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 省份简称与名称映射
+ ///
+ private static readonly Dictionary ProvinceMap = new Dictionary
+ {
+ { "京", "北京市" }, { "津", "天津市" }, { "冀", "河北省" }, { "晋", "山西省" },
+ { "蒙", "内蒙古自治区" }, { "辽", "辽宁省" }, { "吉", "吉林省" }, { "黑", "黑龙江省" },
+ { "沪", "上海市" }, { "苏", "江苏省" }, { "浙", "浙江省" }, { "皖", "安徽省" },
+ { "闽", "福建省" }, { "赣", "江西省" }, { "鲁", "山东省" }, { "豫", "河南省" },
+ { "鄂", "湖北省" }, { "湘", "湖南省" }, { "粤", "广东省" }, { "桂", "广西壮族自治区" },
+ { "琼", "海南省" }, { "川", "四川省" }, { "贵", "贵州省" }, { "云", "云南省" },
+ { "藏", "西藏自治区" }, { "陕", "陕西省" }, { "甘", "甘肃省" }, { "青", "青海省" },
+ { "宁", "宁夏回族自治区" }, { "新", "新疆维吾尔自治区" }, { "渝", "重庆市" },
+ { "港", "香港特别行政区" }, { "澳", "澳门特别行政区" }, { "台", "台湾省" }
+ };
+
+ ///
+ /// 车牌字母与城市映射(部分主要城市)
+ ///
+ private static readonly Dictionary> CityMap = new Dictionary>
+ {
+ { "京", new Dictionary { { "A", "市区" }, { "B", "出租车" }, { "C", "郊区" }, { "D", "警车" }, { "E", "郊区" }, { "F", "郊区" }, { "G", "郊区" }, { "H", "郊区" }, { "J", "郊区" }, { "K", "郊区" }, { "L", "郊区" }, { "M", "郊区" }, { "N", "市区" }, { "P", "市区" }, { "Q", "市区" }, { "Y", "郊区" } } },
+ { "沪", new Dictionary { { "A", "市区" }, { "B", "市区" }, { "C", "郊区" }, { "D", "郊区" }, { "E", "市区" }, { "F", "郊区" }, { "G", "郊区" }, { "H", "郊区" }, { "J", "郊区" }, { "K", "郊区" }, { "L", "郊区" }, { "M", "郊区" }, { "N", "市区" }, { "R", "崇明" } } },
+ { "粤", new Dictionary { { "A", "广州市" }, { "B", "深圳市" }, { "C", "珠海市" }, { "D", "汕头市" }, { "E", "佛山市" }, { "F", "韶关市" }, { "G", "湛江市" }, { "H", "肇庆市" }, { "J", "江门市" }, { "K", "茂名市" }, { "L", "惠州市" }, { "M", "梅州市" }, { "N", "汕尾市" }, { "P", "河源市" }, { "Q", "阳江市" }, { "R", "清远市" }, { "S", "东莞市" }, { "T", "中山市" }, { "U", "潮州市" }, { "V", "揭阳市" }, { "W", "云浮市" }, { "X", "顺德区" }, { "Y", "南海区" }, { "Z", "港澳入境" } } },
+ { "苏", new Dictionary { { "A", "南京市" }, { "B", "无锡市" }, { "C", "徐州市" }, { "D", "常州市" }, { "E", "苏州市" }, { "F", "南通市" }, { "G", "连云港市" }, { "H", "淮安市" }, { "J", "盐城市" }, { "K", "扬州市" }, { "L", "镇江市" }, { "M", "泰州市" }, { "N", "宿迁市" } } },
+ { "浙", new Dictionary { { "A", "杭州市" }, { "B", "宁波市" }, { "C", "温州市" }, { "D", "绍兴市" }, { "E", "湖州市" }, { "F", "嘉兴市" }, { "G", "金华市" }, { "H", "衢州市" }, { "J", "台州市" }, { "K", "丽水市" }, { "L", "舟山市" } } },
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证车牌号是否有效
+ ///
+ /// 车牌号
+ /// 是否有效
+ public static bool IsValid(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+ return IsNormalPlate(normalized) || IsNewEnergyPlate(normalized) ||
+ IsWJPlate(normalized) || IsMilitaryPlate(normalized);
+ }
+
+ ///
+ /// 验证是否为普通车牌(7位)
+ ///
+ /// 车牌号
+ /// 是否为普通车牌
+ public static bool IsNormalPlate(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ return NormalPlateRegex.IsMatch(Normalize(plateNumber)!);
+ }
+
+ ///
+ /// 验证是否为新能源车牌(8位)
+ ///
+ /// 车牌号
+ /// 是否为新能源车牌
+ public static bool IsNewEnergyPlate(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+ return IsSmallNewEnergyPlate(normalized) || IsLargeNewEnergyPlate(normalized);
+ }
+
+ ///
+ /// 验证是否为小型新能源车牌(8位,第3位为D或F)
+ ///
+ /// 车牌号
+ /// 是否为小型新能源车牌
+ public static bool IsSmallNewEnergyPlate(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ return NewEnergySmallRegex.IsMatch(Normalize(plateNumber)!);
+ }
+
+ ///
+ /// 验证是否为大型新能源车牌(8位,第8位为D或F)
+ ///
+ /// 车牌号
+ /// 是否为大型新能源车牌
+ public static bool IsLargeNewEnergyPlate(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ return NewEnergyLargeRegex.IsMatch(Normalize(plateNumber)!);
+ }
+
+ ///
+ /// 验证是否为武警车牌
+ ///
+ /// 车牌号
+ /// 是否为武警车牌
+ public static bool IsWJPlate(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ return WJPlateRegex.IsMatch(Normalize(plateNumber)!);
+ }
+
+ ///
+ /// 验证是否为军队车牌
+ ///
+ /// 车牌号
+ /// 是否为军队车牌
+ public static bool IsMilitaryPlate(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ return MilitaryPlateRegex.IsMatch(Normalize(plateNumber)!);
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取车牌类型
+ ///
+ /// 车牌号
+ /// 车牌类型
+ public static PlateType GetPlateType(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return PlateType.Unknown;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ if (IsSmallNewEnergyPlate(normalized))
+ {
+ return PlateType.NewEnergySmall;
+ }
+
+ if (IsLargeNewEnergyPlate(normalized))
+ {
+ return PlateType.NewEnergyLarge;
+ }
+
+ if (IsNormalPlate(normalized))
+ {
+ return PlateType.Normal;
+ }
+
+ if (IsWJPlate(normalized))
+ {
+ return PlateType.WJ;
+ }
+
+ if (IsMilitaryPlate(normalized))
+ {
+ return PlateType.Military;
+ }
+
+ return PlateType.Unknown;
+ }
+
+ ///
+ /// 验证是否为燃油车车牌(普通7位车牌,非新能源)
+ ///
+ /// 车牌号
+ /// 是否为燃油车车牌
+ public static bool IsFuelVehicle(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return false;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ // 普通车牌(7位)且不是军队/武警车牌 = 燃油车
+ return normalized.Length == 7 && IsNormalPlate(normalized);
+ }
+
+ ///
+ /// 获取车辆燃料类型
+ ///
+ /// 车牌号
+ /// 燃料类型
+ public static FuelType GetFuelType(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return FuelType.Unknown;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ // 燃油车(普通7位车牌)
+ if (IsFuelVehicle(normalized))
+ {
+ return FuelType.Fuel;
+ }
+
+ // 新能源车
+ if (IsNewEnergyPlate(normalized))
+ {
+ NewEnergyType? newEnergyType = GetNewEnergyType(normalized);
+ return newEnergyType switch
+ {
+ NewEnergyType.PureElectric => FuelType.PureElectric,
+ NewEnergyType.PluginHybrid => FuelType.PluginHybrid,
+ _ => FuelType.Unknown
+ };
+ }
+
+ return FuelType.Unknown;
+ }
+
+ ///
+ /// 获取车辆燃料类型名称
+ ///
+ /// 车牌号
+ /// 燃料类型名称
+ public static string? GetFuelTypeName(string? plateNumber)
+ {
+ return GetFuelType(plateNumber) switch
+ {
+ FuelType.Fuel => "燃油车",
+ FuelType.PureElectric => "纯电动",
+ FuelType.PluginHybrid => "插电混动",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取新能源车型类型
+ ///
+ /// 车牌号
+ /// 新能源类型,非新能源车牌返回null
+ public static NewEnergyType? GetNewEnergyType(string? plateNumber)
+ {
+ if (!IsNewEnergyPlate(plateNumber))
+ {
+ return null;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ // 小型新能源车牌:第3位
+ // 大型新能源车牌:第8位
+ char typeChar;
+ if (normalized.Length == 8)
+ {
+ if (normalized[2] == 'D' || normalized[2] == 'F')
+ {
+ typeChar = normalized[2];
+ }
+ else
+ {
+ typeChar = normalized[7];
+ }
+ }
+ else
+ {
+ return null;
+ }
+
+ // D: 纯电动, F: 插电式混合动力
+ return typeChar == 'D' ? NewEnergyType.PureElectric : NewEnergyType.PluginHybrid;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取省份名称
+ ///
+ /// 车牌号
+ /// 省份名称
+ public static string? GetProvince(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return null;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ // 武警车牌特殊处理
+ if (IsWJPlate(normalized))
+ {
+ return "武警";
+ }
+
+ // 军队车牌特殊处理
+ if (IsMilitaryPlate(normalized))
+ {
+ return "军队";
+ }
+
+ string provinceCode = normalized.Substring(0, 1);
+ return ProvinceMap.TryGetValue(provinceCode, out string? province) ? province : null;
+ }
+
+ ///
+ /// 获取城市名称
+ ///
+ /// 车牌号
+ /// 城市名称
+ public static string? GetCity(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return null;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ // 武警或军队车牌无城市信息
+ if (IsWJPlate(normalized) || IsMilitaryPlate(normalized))
+ {
+ return null;
+ }
+
+ string provinceCode = normalized.Substring(0, 1);
+ string cityCode = normalized.Substring(1, 1);
+
+ if (CityMap.TryGetValue(provinceCode, out Dictionary? cities))
+ {
+ if (cities.TryGetValue(cityCode, out string? city))
+ {
+ return city;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取车牌前缀(省份 + 字母)
+ ///
+ /// 车牌号
+ /// 车牌前缀
+ public static string? GetPrefix(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return null;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ if (normalized.Length < 2)
+ {
+ return null;
+ }
+
+ // 普通车牌和新能源车牌:前2位
+ // 武警车牌:前4位(WJ+数字)
+ if (IsWJPlate(normalized))
+ {
+ return normalized.Length >= 4 ? normalized.Substring(0, 4) : null;
+ }
+
+ return normalized.Substring(0, 2);
+ }
+
+ ///
+ /// 获取号码部分(去除前缀)
+ ///
+ /// 车牌号
+ /// 号码部分
+ public static string? GetNumberPart(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return null;
+ }
+
+ string normalized = Normalize(plateNumber)!;
+
+ // 普通车牌:后5位
+ // 新能源车牌:后6位(小型)/ 后6位(大型)
+ // 武警车牌:后5位
+
+ if (IsWJPlate(normalized))
+ {
+ return normalized.Length >= 7 ? normalized.Substring(4) : null;
+ }
+
+ if (normalized.Length == 8)
+ {
+ // 新能源车牌
+ return normalized.Substring(2);
+ }
+
+ if (normalized.Length == 7)
+ {
+ // 普通车牌或军队车牌
+ return normalized.Substring(2);
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化车牌号(转大写,去除特殊字符)
+ ///
+ /// 车牌号
+ /// 格式化后的车牌号
+ public static string? Normalize(string? plateNumber)
+ {
+ if (string.IsNullOrWhiteSpace(plateNumber))
+ {
+ return null;
+ }
+
+ // 去除空格和特殊字符,转大写
+ string normalized = plateNumber.ToUpper().Trim();
+
+ // 保留汉字、字母、数字
+ normalized = Regex.Replace(normalized, @"[^\u4e00-\u9fa5A-Z0-9]", "");
+
+ return normalized;
+ }
+
+ ///
+ /// 格式化车牌号(带分隔符)
+ ///
+ /// 车牌号
+ /// 分隔符(默认为空格)
+ /// 格式化后的车牌号
+ public static string? Format(string? plateNumber, string separator = " ")
+ {
+ string? normalized = Normalize(plateNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ // 武警车牌特殊处理
+ if (IsWJPlate(normalized))
+ {
+ if (normalized.Length == 7)
+ {
+ return normalized.Substring(0, 2) + separator + normalized.Substring(2, 2) + separator + normalized.Substring(4);
+ }
+ return normalized;
+ }
+
+ // 普通车牌:2+5
+ // 新能源车牌:2+6
+ if (normalized.Length == 7)
+ {
+ return normalized.Substring(0, 2) + separator + normalized.Substring(2);
+ }
+
+ if (normalized.Length == 8)
+ {
+ return normalized.Substring(0, 2) + separator + normalized.Substring(2);
+ }
+
+ return normalized;
+ }
+
+ ///
+ /// 车牌号脱敏:粤***123
+ ///
+ /// 车牌号
+ /// 脱敏后的车牌号
+ public static string? Mask(string? plateNumber)
+ {
+ string? normalized = Normalize(plateNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ // 武警车牌特殊处理
+ if (IsWJPlate(normalized))
+ {
+ if (normalized.Length >= 7)
+ {
+ return normalized.Substring(0, 4) + "***" + normalized.Substring(normalized.Length - 2);
+ }
+ return null;
+ }
+
+ if (normalized.Length == 7)
+ {
+ // 普通车牌:保留省份 + 后2位
+ return normalized.Substring(0, 1) + "***" + normalized.Substring(5);
+ }
+
+ if (normalized.Length == 8)
+ {
+ // 新能源车牌:保留省份 + 后2位
+ return normalized.Substring(0, 1) + "****" + normalized.Substring(6);
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机车牌号(仅供测试使用)
+ ///
+ /// 省份简称(可选,默认随机)
+ /// 是否为新能源车牌(可选,默认随机)
+ /// 车牌号
+ public static string GenerateRandom(string? province = null, bool? isNewEnergy = null)
+ {
+ // 省份
+ string[] provinces = { "京", "津", "冀", "晋", "蒙", "辽", "吉", "黑", "沪", "苏", "浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "渝" };
+ string prov = province ?? MathCategory.RandomUtil.GetRandomElement(provinces);
+
+ // 字母
+ const string letters = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // 不包含I和O
+ string letter = MathCategory.RandomUtil.GetRandomElement(letters.ToCharArray()).ToString();
+
+ bool newEnergy = isNewEnergy ?? MathCategory.RandomUtil.RandomBool();
+
+ if (newEnergy)
+ {
+ // 新能源车牌(8位)
+ char energyType = MathCategory.RandomUtil.RandomBool() ? 'D' : 'F';
+ string numbers = GenerateRandomAlphanumeric(5);
+ return prov + letter + energyType + numbers;
+ }
+ else
+ {
+ // 普通车牌(7位)
+ string numbers = GenerateRandomAlphanumeric(5);
+ return prov + letter + numbers;
+ }
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 生成随机字母数字组合
+ ///
+ private static string GenerateRandomAlphanumeric(int length)
+ {
+ const string chars = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"; // 不包含I和O
+ string result = "";
+ for (int i = 0; i < length; i++)
+ {
+ result += MathCategory.RandomUtil.GetRandomElement(chars.ToCharArray());
+ }
+ return result;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/MACAddressUtil.cs b/EasyTool.Core/BusinessCategory/MACAddressUtil.cs
new file mode 100644
index 0000000..0bd23a3
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/MACAddressUtil.cs
@@ -0,0 +1,508 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// MAC地址工具类
+ ///
+ public static class MACAddressUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// MAC地址正则表达式(多种格式)
+ ///
+ private static readonly Regex[] MACRegexes =
+ {
+ // XX:XX:XX:XX:XX:XX
+ new(@"^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$", RegexOptions.Compiled),
+ // XX-XX-XX-XX-XX-XX
+ new(@"^([0-9A-Fa-f]{2}[-]){5}[0-9A-Fa-f]{2}$", RegexOptions.Compiled),
+ // XXXX.XXXX.XXXX (Cisco格式)
+ new(@"^([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}$", RegexOptions.Compiled),
+ // XXXXXXXXXXXX (无分隔符)
+ new(@"^[0-9A-Fa-f]{12}$", RegexOptions.Compiled)
+ };
+
+ ///
+ /// OUI(组织唯一标识符)与厂商映射(部分)
+ ///
+ private static readonly (string Prefix, string Vendor)[] OuiPrefixMap =
+ {
+ // Apple
+ ("00:03:93", "Apple"), ("00:05:02", "Apple"), ("00:0A:27", "Apple"),
+ ("00:0A:95", "Apple"), ("00:0D:93", "Apple"), ("00:0E:B2", "Apple"),
+ ("00:11:24", "Apple"), ("00:14:51", "Apple"), ("00:16:CB", "Apple"),
+ ("00:17:F2", "Apple"), ("00:19:E3", "Apple"), ("00:1B:63", "Apple"),
+ ("00:1C:B3", "Apple"), ("00:1D:4F", "Apple"), ("00:1E:52", "Apple"),
+ ("00:1F:5B", "Apple"), ("00:1F:F3", "Apple"), ("00:22:41", "Apple"),
+ ("00:23:12", "Apple"), ("00:23:32", "Apple"), ("00:23:6C", "Apple"),
+ ("00:23:DF", "Apple"), ("00:24:36", "Apple"), ("00:25:00", "Apple"),
+ ("00:25:4B", "Apple"), ("00:25:BC", "Apple"), ("00:26:08", "Apple"),
+ ("00:26:4A", "Apple"), ("00:26:B0", "Apple"), ("00:26:BB", "Apple"),
+ ("00:26:DB", "Apple"), ("A4:83:E7", "Apple"), ("AC:87:A3", "Apple"),
+ ("DC:A9:04", "Apple"), ("F0:DB:E2", "Apple"),
+
+ // Samsung
+ ("00:07:AB", "Samsung"), ("00:0D:E5", "Samsung"), ("00:12:47", "Samsung"),
+ ("00:13:77", "Samsung"), ("00:15:99", "Samsung"), ("00:16:6B", "Samsung"),
+ ("00:17:C9", "Samsung"), ("00:18:AF", "Samsung"), ("00:1A:8A", "Samsung"),
+ ("00:1B:59", "Samsung"), ("00:1D:2B", "Samsung"), ("00:1E:7D", "Samsung"),
+ ("00:1F:36", "Samsung"), ("00:22:43", "Samsung"), ("00:24:90", "Samsung"),
+ ("00:25:38", "Samsung"), ("08:EC:A9", "Samsung"), ("30:07:4D", "Samsung"),
+ ("34:23:87", "Samsung"), ("38:2D:D8", "Samsung"), ("40:4D:7F", "Samsung"),
+ ("54:88:0E", "Samsung"), ("5C:8D:4E", "Samsung"), ("64:5D:86", "Samsung"),
+ ("6C:5C:14", "Samsung"), ("7C:1E:52", "Samsung"), ("88:36:6C", "Samsung"),
+ ("8C:10:D4", "Samsung"), ("94:8B:C1", "Samsung"), ("98:F0:AB", "Samsung"),
+ ("A0:10:81", "Samsung"), ("B0:DF:3A", "Samsung"), ("CC:61:E5", "Samsung"),
+ ("D8:A2:5E", "Samsung"), ("E0:D5:5E", "Samsung"), ("E8:50:8B", "Samsung"),
+ ("F0:27:65", "Samsung"),
+
+ // Huawei
+ ("00:0F:B5", "Huawei"), ("00:18:82", "Huawei"), ("00:1E:10", "Huawei"),
+ ("00:25:68", "Huawei"), ("08:57:00", "Huawei"), ("0C:96:BF", "Huawei"),
+ ("10:1D:59", "Huawei"), ("18:3E:0F", "Huawei"), ("28:ED:6A", "Huawei"),
+ ("2C:B0:5D", "Huawei"), ("34:A3:95", "Huawei"), ("38:F8:5B", "Huawei"),
+ ("40:4E:36", "Huawei"), ("44:8B:CE", "Huawei"), ("48:37:B9", "Huawei"),
+ ("4C:FB:9F", "Huawei"), ("50:01:BB", "Huawei"), ("54:BF:64", "Huawei"),
+ ("58:00:E3", "Huawei"), ("5C:FE:45", "Huawei"), ("64:9A:BE", "Huawei"),
+ ("68:DB:CA", "Huawei"), ("6C:5D:43", "Huawei"), ("70:19:0F", "Huawei"),
+ ("74:6A:D8", "Huawei"), ("78:44:76", "Huawei"), ("7C:1E:52", "Huawei"),
+ ("80:8D:3F", "Huawei"), ("84:10:0D", "Huawei"), ("88:43:E1", "Huawei"),
+ ("8C:71:F8", "Huawei"), ("90:2B:D2", "Huawei"), ("94:FE:22", "Huawei"),
+ ("98:6F:1A", "Huawei"), ("9C:2E:A1", "Huawei"), ("A0:8F:85", "Huawei"),
+ ("A4:4E:31", "Huawei"), ("B0:5B:48", "Huawei"), ("B4:69:21", "Huawei"),
+ ("C0:BD:D1", "Huawei"), ("C4:4E:AC", "Huawei"), ("C8:5B:76", "Huawei"),
+ ("CC:34:29", "Huawei"), ("D0:59:E4", "Huawei"), ("D4:7A:34", "Huawei"),
+ ("DC:72:9B", "Huawei"), ("E0:37:BF", "Huawei"), ("E4:0D:73", "Huawei"),
+ ("E8:4E:CE", "Huawei"), ("EC:9A:74", "Huawei"), ("F0:FE:6B", "Huawei"),
+ ("FC:2C:55", "Huawei"),
+
+ // Xiaomi
+ ("00:BB:3E", "Xiaomi"), ("10:2A:B3", "Xiaomi"), ("18:59:36", "Xiaomi"),
+ ("20:82:C0", "Xiaomi"), ("24:F9:A3", "Xiaomi"), ("28:ED:E1", "Xiaomi"),
+ ("34:80:B3", "Xiaomi"), ("38:1A:21", "Xiaomi"), ("3C:BD:D8", "Xiaomi"),
+ ("40:31:3C", "Xiaomi"), ("44:6F:D1", "Xiaomi"), ("48:88:CA", "Xiaomi"),
+ ("4C:18:D6", "Xiaomi"), ("50:1E:2D", "Xiaomi"), ("50:EC:50", "Xiaomi"),
+ ("58:44:98", "Xiaomi"), ("64:90:C1", "Xiaomi"), ("6C:5C:14", "Xiaomi"),
+ ("6C:8D:C1", "Xiaomi"), ("74:A3:E4", "Xiaomi"), ("78:02:F8", "Xiaomi"),
+ ("7C:1D:D9", "Xiaomi"), ("7C:8B:CA", "Xiaomi"), ("88:0F:10", "Xiaomi"),
+ ("8C:4C:4B", "Xiaomi"), ("8C:F6:79", "Xiaomi"), ("90:82:37", "Xiaomi"),
+ ("94:87:E0", "Xiaomi"), ("98:0C:82", "Xiaomi"), ("9C:2E:A1", "Xiaomi"),
+ ("9C:99:A0", "Xiaomi"), ("A0:CB:FD", "Xiaomi"), ("A4:4E:31", "Xiaomi"),
+ ("AC:29:3A", "Xiaomi"), ("B0:E2:35", "Xiaomi"), ("B8:C1:11", "Xiaomi"),
+ ("C0:26:0D", "Xiaomi"), ("C0:EE:FB", "Xiaomi"), ("C4:0B:CB", "Xiaomi"),
+ ("C4:4C:CA", "Xiaomi"), ("C8:1E:E7", "Xiaomi"), ("C8:94:BB", "Xiaomi"),
+ ("CC:AF:78", "Xiaomi"), ("D0:D2:B0", "Xiaomi"), ("D4:5D:64", "Xiaomi"),
+ ("D8:1C:79", "Xiaomi"), ("D8:96:95", "Xiaomi"), ("DC:A6:32", "Xiaomi"),
+ ("E0:46:44", "Xiaomi"), ("E4:B2:1F", "Xiaomi"), ("EC:3A:FD", "Xiaomi"),
+ ("EC:41:18", "Xiaomi"), ("F0:B4:29", "Xiaomi"), ("F4:28:53", "Xiaomi"),
+ ("F8:A4:5F", "Xiaomi"), ("FC:6D:B3", "Xiaomi"), ("FC:A6:67", "Xiaomi"),
+
+ // Intel
+ ("00:02:B3", "Intel"), ("00:03:47", "Intel"), ("00:04:23", "Intel"),
+ ("00:07:E9", "Intel"), ("00:0B:DB", "Intel"), ("00:0D:DA", "Intel"),
+ ("00:0E:0C", "Intel"), ("00:0E:35", "Intel"), ("00:0E:A6", "Intel"),
+ ("00:0F:B0", "Intel"), ("00:0F:EE", "Intel"), ("00:10:E0", "Intel"),
+ ("00:11:0A", "Intel"), ("00:11:11", "Intel"), ("00:11:43", "Intel"),
+ ("00:11:F5", "Intel"), ("00:12:3F", "Intel"), ("00:13:20", "Intel"),
+ ("00:13:CE", "Intel"), ("00:13:E8", "Intel"), ("00:14:22", "Intel"),
+ ("00:14:78", "Intel"), ("00:14:A5", "Intel"), ("00:15:17", "Intel"),
+ ("00:15:C5", "Intel"), ("00:16:76", "Intel"), ("00:16:B6", "Intel"),
+ ("00:17:08", "Intel"), ("00:17:9A", "Intel"), ("00:17:C2", "Intel"),
+ ("00:18:13", "Intel"), ("00:18:68", "Intel"), ("00:18:DE", "Intel"),
+ ("00:19:D1", "Intel"), ("00:1B:21", "Intel"), ("00:1C:BD", "Intel"),
+ ("00:1D:72", "Intel"), ("00:1E:64", "Intel"), ("00:1E:67", "Intel"),
+ ("00:1F:16", "Intel"), ("00:1F:29", "Intel"), ("00:21:5C", "Intel"),
+ ("00:21:CC", "Intel"), ("00:22:FA", "Intel"), ("00:23:14", "Intel"),
+ ("00:23:7E", "Intel"), ("00:23:AE", "Intel"), ("00:24:D7", "Intel"),
+ ("00:25:66", "Intel"), ("00:26:B7", "Intel"), ("00:26:C6", "Intel"),
+ ("00:26:C7", "Intel"), ("00:27:0E", "Intel"), ("00:30:1B", "Intel"),
+
+ // Cisco
+ ("00:00:0C", "Cisco"), ("00:01:42", "Cisco"), ("00:01:43", "Cisco"),
+ ("00:01:63", "Cisco"), ("00:01:64", "Cisco"), ("00:01:96", "Cisco"),
+ ("00:01:97", "Cisco"), ("00:01:C7", "Cisco"), ("00:02:16", "Cisco"),
+ ("00:02:17", "Cisco"), ("00:02:4A", "Cisco"), ("00:02:7D", "Cisco"),
+ ("00:02:7E", "Cisco"), ("00:02:FD", "Cisco"), ("00:03:6B", "Cisco"),
+ ("00:03:6F", "Cisco"), ("00:03:E3", "Cisco"), ("00:04:27", "Cisco"),
+ ("00:04:C1", "Cisco"), ("00:05:30", "Cisco"), ("00:05:32", "Cisco"),
+ ("00:05:59", "Cisco"), ("00:05:85", "Cisco"), ("00:05:9A", "Cisco"),
+ ("00:05:DC", "Cisco"), ("00:06:28", "Cisco"), ("00:06:52", "Cisco"),
+ ("00:06:53", "Cisco"), ("00:07:0D", "Cisco"), ("00:07:0E", "Cisco"),
+ ("00:07:0F", "Cisco"), ("00:07:50", "Cisco"), ("00:07:EC", "Cisco"),
+ ("00:08:21", "Cisco"), ("00:08:22", "Cisco"), ("00:08:24", "Cisco"),
+ ("00:08:2C", "Cisco"), ("00:08:A3", "Cisco"), ("00:09:0C", "Cisco"),
+ ("00:09:0D", "Cisco"), ("00:09:41", "Cisco"), ("00:09:43", "Cisco"),
+ ("00:09:44", "Cisco"), ("00:09:7C", "Cisco"), ("00:09:B7", "Cisco"),
+ ("00:0A:B8", "Cisco"), ("00:0A:F4", "Cisco"), ("00:0B:5F", "Cisco"),
+ ("00:0B:BE", "Cisco"), ("00:0B:FD", "Cisco"), ("00:0C:0C", "Cisco"),
+ ("00:0C:30", "Cisco"), ("00:0C:31", "Cisco"), ("00:0C:CE", "Cisco"),
+ ("00:0D:28", "Cisco"), ("00:0D:29", "Cisco"), ("00:0D:62", "Cisco"),
+ ("00:0D:63", "Cisco"), ("00:0D:64", "Cisco"), ("00:0D:BD", "Cisco"),
+ ("00:0D:BE", "Cisco"), ("00:0D:BF", "Cisco"), ("00:0D:C0", "Cisco"),
+ ("00:0E:0C", "Cisco"), ("00:0E:38", "Cisco"), ("00:0E:39", "Cisco"),
+ ("00:0E:3A", "Cisco"), ("00:0E:3B", "Cisco"), ("00:0E:3C", "Cisco"),
+ ("00:0E:84", "Cisco"), ("00:0F:23", "Cisco"), ("00:0F:24", "Cisco"),
+ ("00:0F:34", "Cisco"), ("00:0F:35", "Cisco"), ("00:0F:F7", "Cisco"),
+ ("00:0F:F8", "Cisco"), ("00:10:0C", "Cisco"), ("00:10:0D", "Cisco"),
+ ("00:10:0E", "Cisco"), ("00:10:0F", "Cisco"), ("00:10:54", "Cisco"),
+ ("00:10:58", "Cisco"), ("00:10:7A", "Cisco"), ("00:10:7B", "Cisco"),
+ ("00:10:E8", "Cisco"), ("00:10:F3", "Cisco"), ("00:10:F6", "Cisco"),
+ ("00:11:1B", "Cisco"), ("00:11:20", "Cisco"), ("00:11:21", "Cisco"),
+ ("00:11:2F", "Cisco"), ("00:11:30", "Cisco"), ("00:11:90", "Cisco"),
+ ("00:11:91", "Cisco"), ("00:11:92", "Cisco"), ("00:11:93", "Cisco"),
+ ("00:11:BB", "Cisco"), ("00:11:BC", "Cisco"), ("00:11:BD", "Cisco"),
+ ("00:11:BE", "Cisco"), ("00:11:BF", "Cisco"), ("00:11:FA", "Cisco"),
+ ("00:11:FB", "Cisco"), ("00:11:FC", "Cisco"), ("00:11:FD", "Cisco"),
+ ("00:11:FE", "Cisco"), ("00:12:00", "Cisco"), ("00:12:01", "Cisco"),
+ ("00:12:17", "Cisco"), ("00:12:1C", "Cisco"), ("00:12:1D", "Cisco"),
+ ("00:12:40", "Cisco"), ("00:12:41", "Cisco"), ("00:12:43", "Cisco"),
+ ("00:12:7F", "Cisco"), ("00:12:80", "Cisco"), ("00:12:DA", "Cisco"),
+ ("00:12:DB", "Cisco"), ("00:12:DC", "Cisco"), ("00:12:F9", "Cisco"),
+ ("00:12:FA", "Cisco"), ("00:13:1A", "Cisco"), ("00:13:1B", "Cisco"),
+ ("00:13:1C", "Cisco"), ("00:13:19", "Cisco"), ("00:13:46", "Cisco"),
+ ("00:13:47", "Cisco"), ("00:13:48", "Cisco"), ("00:13:49", "Cisco"),
+ ("00:13:5F", "Cisco"), ("00:13:60", "Cisco"), ("00:13:61", "Cisco"),
+ ("00:13:7F", "Cisco"), ("00:13:80", "Cisco"), ("00:13:81", "Cisco"),
+ ("00:13:C3", "Cisco"), ("00:13:C4", "Cisco"), ("00:13:C5", "Cisco"),
+ ("00:13:E8", "Cisco"), ("00:13:F7", "Cisco"), ("00:14:1B", "Cisco"),
+ ("00:14:69", "Cisco"), ("00:14:6A", "Cisco"), ("00:14:6B", "Cisco"),
+ ("00:14:97", "Cisco"), ("00:14:9A", "Cisco"), ("00:14:A1", "Cisco"),
+ ("00:14:A2", "Cisco"), ("00:14:BF", "Cisco"), ("00:14:F1", "Cisco"),
+ ("00:14:F2", "Cisco"), ("00:15:0C", "Cisco"), ("00:15:17", "Cisco"),
+ ("00:15:1B", "Cisco"), ("00:15:1C", "Cisco"), ("00:15:2B", "Cisco"),
+ ("00:15:60", "Cisco"), ("00:15:61", "Cisco"), ("00:15:62", "Cisco"),
+ ("00:15:63", "Cisco"), ("00:15:FA", "Cisco"), ("00:15:FB", "Cisco"),
+ ("00:15:FC", "Cisco"), ("00:15:FD", "Cisco"), ("00:16:35", "Cisco"),
+ ("00:16:36", "Cisco"), ("00:16:37", "Cisco"), ("00:16:46", "Cisco"),
+ ("00:16:47", "Cisco"), ("00:16:48", "Cisco"), ("00:16:78", "Cisco"),
+ ("00:16:79", "Cisco"), ("00:16:9D", "Cisco"), ("00:16:9E", "Cisco"),
+ ("00:16:C6", "Cisco"), ("00:16:C7", "Cisco"), ("00:16:C8", "Cisco"),
+ ("00:17:0D", "Cisco"), ("00:17:0E", "Cisco"), ("00:17:0F", "Cisco"),
+ ("00:17:59", "Cisco"), ("00:17:5A", "Cisco"), ("00:17:5B", "Cisco"),
+ ("00:17:84", "Cisco"), ("00:17:85", "Cisco"), ("00:17:86", "Cisco"),
+ ("00:17:94", "Cisco"), ("00:17:95", "Cisco"), ("00:17:96", "Cisco"),
+ ("00:17:DF", "Cisco"), ("00:17:E0", "Cisco"), ("00:17:E1", "Cisco"),
+ ("00:18:71", "Cisco"), ("00:18:72", "Cisco"), ("00:18:73", "Cisco"),
+ ("00:18:81", "Cisco"), ("00:18:82", "Cisco"), ("00:18:83", "Cisco"),
+ ("00:18:AF", "Cisco"), ("00:18:B9", "Cisco"), ("00:18:BA", "Cisco"),
+ ("00:18:BB", "Cisco"), ("00:19:06", "Cisco"), ("00:19:07", "Cisco"),
+ ("00:19:2F", "Cisco"), ("00:19:30", "Cisco"), ("00:19:55", "Cisco"),
+ ("00:19:56", "Cisco"), ("00:19:57", "Cisco"), ("00:19:68", "Cisco"),
+ ("00:19:69", "Cisco"), ("00:19:6A", "Cisco"), ("00:19:85", "Cisco"),
+ ("00:19:86", "Cisco"), ("00:19:87", "Cisco"), ("00:19:A9", "Cisco"),
+ ("00:19:AA", "Cisco"), ("00:19:AB", "Cisco"), ("00:19:E7", "Cisco"),
+ ("00:19:E8", "Cisco"), ("00:19:E9", "Cisco"), ("00:1A:0D", "Cisco"),
+ ("00:1A:0E", "Cisco"), ("00:1A:0F", "Cisco"), ("00:1A:2F", "Cisco"),
+ ("00:1A:30", "Cisco"), ("00:1A:31", "Cisco"), ("00:1A:6B", "Cisco"),
+ ("00:1A:6C", "Cisco"), ("00:1A:6D", "Cisco"), ("00:1A:A0", "Cisco"),
+ ("00:1A:A1", "Cisco"), ("00:1A:A2", "Cisco"), ("00:1A:A3", "Cisco"),
+ ("00:1A:E1", "Cisco"), ("00:1A:E2", "Cisco"), ("00:1A:E3", "Cisco"),
+ ("00:1B:0D", "Cisco"), ("00:1B:0E", "Cisco"), ("00:1B:0F", "Cisco"),
+ ("00:1B:53", "Cisco"), ("00:1B:54", "Cisco"), ("00:1B:55", "Cisco"),
+ ("00:1B:8C", "Cisco"), ("00:1B:8D", "Cisco"), ("00:1B:8E", "Cisco"),
+ ("00:1B:D4", "Cisco"), ("00:1B:D5", "Cisco"), ("00:1B:D6", "Cisco"),
+ ("00:1C:0E", "Cisco"), ("00:1C:0F", "Cisco"), ("00:1C:10", "Cisco"),
+ ("00:1C:58", "Cisco"), ("00:1C:59", "Cisco"), ("00:1C:5A", "Cisco"),
+ ("00:1C:B0", "Cisco"), ("00:1C:B1", "Cisco"), ("00:1C:B2", "Cisco"),
+ ("00:1C:F0", "Cisco"), ("00:1C:F1", "Cisco"), ("00:1C:F2", "Cisco"),
+ ("00:1D:0F", "Cisco"), ("00:1D:10", "Cisco"), ("00:1D:11", "Cisco"),
+ ("00:1D:45", "Cisco"), ("00:1D:46", "Cisco"), ("00:1D:47", "Cisco"),
+ ("00:1D:9C", "Cisco"), ("00:1D:9D", "Cisco"), ("00:1D:9E", "Cisco"),
+ ("00:1D:E2", "Cisco"), ("00:1D:E3", "Cisco"), ("00:1D:E4", "Cisco"),
+ ("00:1E:13", "Cisco"), ("00:1E:14", "Cisco"), ("00:1E:15", "Cisco"),
+ ("00:1E:49", "Cisco"), ("00:1E:4A", "Cisco"), ("00:1E:4B", "Cisco"),
+ ("00:1E:79", "Cisco"), ("00:1E:7A", "Cisco"), ("00:1E:7B", "Cisco"),
+ ("00:1E:B4", "Cisco"), ("00:1E:B5", "Cisco"), ("00:1E:B6", "Cisco"),
+ ("00:1F:1D", "Cisco"), ("00:1F:1E", "Cisco"), ("00:1F:1F", "Cisco"),
+ ("00:1F:6C", "Cisco"), ("00:1F:6D", "Cisco"), ("00:1F:6E", "Cisco"),
+ ("00:1F:9D", "Cisco"), ("00:1F:9E", "Cisco"), ("00:1F:9F", "Cisco"),
+ ("00:1F:C8", "Cisco"), ("00:1F:C9", "Cisco"), ("00:1F:CA", "Cisco"),
+ ("00:21:0D", "Cisco"), ("00:21:0E", "Cisco"), ("00:21:0F", "Cisco"),
+ ("00:21:55", "Cisco"), ("00:21:56", "Cisco"), ("00:21:57", "Cisco"),
+ ("00:21:A0", "Cisco"), ("00:21:A1", "Cisco"), ("00:21:A2", "Cisco"),
+ ("00:21:D5", "Cisco"), ("00:21:D6", "Cisco"), ("00:21:D7", "Cisco"),
+ ("00:22:55", "Cisco"), ("00:22:56", "Cisco"), ("00:22:57", "Cisco"),
+ ("00:22:90", "Cisco"), ("00:22:91", "Cisco"), ("00:22:92", "Cisco"),
+ ("00:22:BD", "Cisco"), ("00:22:BE", "Cisco"), ("00:22:BF", "Cisco"),
+ ("00:23:04", "Cisco"), ("00:23:05", "Cisco"), ("00:23:06", "Cisco"),
+ ("00:23:33", "Cisco"), ("00:23:34", "Cisco"), ("00:23:35", "Cisco"),
+ ("00:23:5C", "Cisco"), ("00:23:5D", "Cisco"), ("00:23:5E", "Cisco"),
+ ("00:23:EB", "Cisco"), ("00:23:EC", "Cisco"), ("00:23:ED", "Cisco"),
+ ("00:24:13", "Cisco"), ("00:24:14", "Cisco"), ("00:24:15", "Cisco"),
+ ("00:24:50", "Cisco"), ("00:24:51", "Cisco"), ("00:24:52", "Cisco"),
+ ("00:24:97", "Cisco"), ("00:24:98", "Cisco"), ("00:24:99", "Cisco"),
+ ("00:24:B2", "Cisco"), ("00:24:B3", "Cisco"), ("00:24:B4", "Cisco"),
+ ("00:24:F7", "Cisco"), ("00:24:F8", "Cisco"), ("00:24:F9", "Cisco"),
+ ("00:25:1B", "Cisco"), ("00:25:1C", "Cisco"), ("00:25:1D", "Cisco"),
+ ("00:25:2A", "Cisco"), ("00:25:2B", "Cisco"), ("00:25:2C", "Cisco"),
+ ("00:25:61", "Cisco"), ("00:25:62", "Cisco"), ("00:25:63", "Cisco"),
+ ("00:25:84", "Cisco"), ("00:25:85", "Cisco"), ("00:25:86", "Cisco"),
+ ("00:25:B5", "Cisco"), ("00:25:B6", "Cisco"), ("00:25:B7", "Cisco"),
+ ("00:26:0B", "Cisco"), ("00:26:0C", "Cisco"), ("00:26:0D", "Cisco"),
+ ("00:26:51", "Cisco"), ("00:26:52", "Cisco"), ("00:26:53", "Cisco"),
+ ("00:26:88", "Cisco"), ("00:26:89", "Cisco"), ("00:26:8A", "Cisco"),
+ ("00:26:99", "Cisco"), ("00:26:9A", "Cisco"), ("00:26:9B", "Cisco"),
+ ("00:26:CA", "Cisco"), ("00:26:CB", "Cisco"), ("00:26:CC", "Cisco"),
+ ("00:50:56", "VMware"), ("00:0C:29", "VMware"), ("00:05:69", "VMware"),
+ ("00:1C:14", "VMware"), ("00:50:56", "VMware")
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证MAC地址是否有效
+ ///
+ /// MAC地址
+ /// 是否有效
+ public static bool IsValid(string? mac)
+ {
+ if (string.IsNullOrWhiteSpace(mac))
+ {
+ return false;
+ }
+
+ foreach (var regex in MACRegexes)
+ {
+ if (regex.IsMatch(mac))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取OUI(组织唯一标识符,前3字节)
+ ///
+ /// MAC地址
+ /// OUI
+ public static string? GetOUI(string? mac)
+ {
+ if (!IsValid(mac))
+ {
+ return null;
+ }
+
+ string clean = Clean(mac)!;
+ return clean.Substring(0, 6).ToUpper();
+ }
+
+ ///
+ /// 获取设备标识符(后3字节)
+ ///
+ /// MAC地址
+ /// 设备标识符
+ public static string? GetDeviceId(string? mac)
+ {
+ if (!IsValid(mac))
+ {
+ return null;
+ }
+
+ string clean = Clean(mac)!;
+ return clean.Substring(6, 6).ToUpper();
+ }
+
+ ///
+ /// 获取厂商名称
+ ///
+ /// MAC地址
+ /// 厂商名称
+ public static string? GetVendor(string? mac)
+ {
+ string? oui = GetOUI(mac);
+ if (oui == null)
+ {
+ return null;
+ }
+
+ // 格式化为XX:XX:XX格式进行查找
+ string formattedOui = $"{oui.Substring(0, 2)}:{oui.Substring(2, 2)}:{oui.Substring(4, 2)}".ToUpper();
+
+ foreach (var mapping in OuiPrefixMap)
+ {
+ if (mapping.Prefix.Equals(formattedOui, StringComparison.OrdinalIgnoreCase))
+ {
+ return mapping.Vendor;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 判断是否为组播地址
+ ///
+ /// MAC地址
+ /// 是否为组播地址
+ public static bool IsMulticast(string? mac)
+ {
+ if (!IsValid(mac))
+ {
+ return false;
+ }
+
+ string clean = Clean(mac)!;
+ // 第一个字节的最低位为1表示组播
+ int firstByte = Convert.ToInt32(clean.Substring(0, 2), 16);
+ return (firstByte & 0x01) == 1;
+ }
+
+ ///
+ /// 判断是否为广播地址(FF:FF:FF:FF:FF:FF)
+ ///
+ /// MAC地址
+ /// 是否为广播地址
+ public static bool IsBroadcast(string? mac)
+ {
+ string? clean = Clean(mac);
+ return clean == "FFFFFFFFFFFF";
+ }
+
+ ///
+ /// 判断是否为本地管理地址
+ ///
+ /// MAC地址
+ /// 是否为本地管理地址
+ public static bool IsLocallyAdministered(string? mac)
+ {
+ if (!IsValid(mac))
+ {
+ return false;
+ }
+
+ string clean = Clean(mac)!;
+ // 第一个字节的次低位为1表示本地管理
+ int firstByte = Convert.ToInt32(clean.Substring(0, 2), 16);
+ return (firstByte & 0x02) == 2;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 清理MAC地址(去除分隔符)
+ ///
+ /// MAC地址
+ /// 12位十六进制字符串
+ public static string? Clean(string? mac)
+ {
+ if (string.IsNullOrWhiteSpace(mac))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(mac, @"[^\dA-Fa-f]", "").ToUpper();
+ return cleaned.Length == 12 ? cleaned : null;
+ }
+
+ ///
+ /// 格式化为标准格式(XX:XX:XX:XX:XX:XX)
+ ///
+ /// MAC地址
+ /// 格式化后的MAC地址
+ public static string? Format(string? mac)
+ {
+ string? clean = Clean(mac);
+ if (clean == null)
+ {
+ return null;
+ }
+
+ return $"{clean.Substring(0, 2)}:{clean.Substring(2, 2)}:{clean.Substring(4, 2)}:" +
+ $"{clean.Substring(6, 2)}:{clean.Substring(8, 2)}:{clean.Substring(10, 2)}";
+ }
+
+ ///
+ /// 格式化为横线分隔(XX-XX-XX-XX-XX-XX)
+ ///
+ /// MAC地址
+ /// 格式化后的MAC地址
+ public static string? FormatWithHyphens(string? mac)
+ {
+ return Format(mac)?.Replace(':', '-');
+ }
+
+ ///
+ /// 格式化为Cisco格式(XXXX.XXXX.XXXX)
+ ///
+ /// MAC地址
+ /// 格式化后的MAC地址
+ public static string? FormatCisco(string? mac)
+ {
+ string? clean = Clean(mac);
+ if (clean == null)
+ {
+ return null;
+ }
+
+ return $"{clean.Substring(0, 4)}.{clean.Substring(4, 4)}.{clean.Substring(8, 4)}";
+ }
+
+ ///
+ /// MAC地址脱敏:AA:BB:**:**:**:FF
+ ///
+ /// MAC地址
+ /// 脱敏后的MAC地址
+ public static string? Mask(string? mac)
+ {
+ string? clean = Clean(mac);
+ if (clean == null)
+ {
+ return null;
+ }
+
+ return $"{clean.Substring(0, 2)}:{clean.Substring(2, 2)}:**:**:**:{clean.Substring(10, 2)}";
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机MAC地址(仅供测试使用)
+ ///
+ /// 厂商OUI(可选,默认随机生成)
+ /// MAC地址
+ public static string GenerateRandom(string? vendor = null)
+ {
+ string oui;
+ string deviceId;
+
+ if (!string.IsNullOrWhiteSpace(vendor) && vendor.Length >= 6)
+ {
+ oui = vendor.Substring(0, 6).ToUpper();
+ }
+ else
+ {
+ // 随机生成OUI(设置本地管理位)
+ int firstByte = MathCategory.RandomUtil.RandomInt(0, 255) | 0x02; // 设置本地管理位
+ oui = firstByte.ToString("X2") + MathCategory.RandomUtil.RandomInt(0, 255).ToString("X2") +
+ MathCategory.RandomUtil.RandomInt(0, 255).ToString("X2");
+ }
+
+ // 随机生成设备ID
+ deviceId = MathCategory.RandomUtil.RandomInt(0, 255).ToString("X2") +
+ MathCategory.RandomUtil.RandomInt(0, 255).ToString("X2") +
+ MathCategory.RandomUtil.RandomInt(0, 255).ToString("X2");
+
+ string clean = oui + deviceId;
+ return $"{clean.Substring(0, 2)}:{clean.Substring(2, 2)}:{clean.Substring(4, 2)}:" +
+ $"{clean.Substring(6, 2)}:{clean.Substring(8, 2)}:{clean.Substring(10, 2)}";
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/OrgCodeUtil.cs b/EasyTool.Core/BusinessCategory/OrgCodeUtil.cs
new file mode 100644
index 0000000..ab168f2
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/OrgCodeUtil.cs
@@ -0,0 +1,254 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 组织机构代码工具类
+ ///
+ public static class OrgCodeUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 组织机构代码正则表达式(9位:8位数字/字母 + 1位校验码)
+ ///
+ private static readonly Regex OrgCodeRegex = new(
+ @"^[A-Z0-9]{8}-?[A-X0-9]$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 组织机构代码字符值映射
+ ///
+ private static readonly int[] CharWeights = { 3, 7, 9, 10, 5, 8, 4, 2 };
+
+ ///
+ /// 校验码对照表
+ ///
+ private const string CheckCodes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证组织机构代码是否有效
+ ///
+ /// 组织机构代码
+ /// 是否有效
+ public static bool IsValid(string? orgCode)
+ {
+ if (string.IsNullOrWhiteSpace(orgCode))
+ {
+ return false;
+ }
+
+ string code = orgCode.ToUpper().Replace("-", "");
+
+ if (code.Length != 9)
+ {
+ return false;
+ }
+
+ if (!OrgCodeRegex.IsMatch(code))
+ {
+ return false;
+ }
+
+ // 计算校验码
+ char? expectedCheck = CalculateCheckCode(code.Substring(0, 8));
+ return expectedCheck.HasValue && expectedCheck.Value == code[8];
+ }
+
+ ///
+ /// 验证格式是否正确(不校验校验位)
+ ///
+ /// 组织机构代码
+ /// 格式是否正确
+ public static bool IsValidFormat(string? orgCode)
+ {
+ if (string.IsNullOrWhiteSpace(orgCode))
+ {
+ return false;
+ }
+
+ string code = orgCode.ToUpper().Replace("-", "");
+ return code.Length == 9 && OrgCodeRegex.IsMatch(code);
+ }
+
+ ///
+ /// 计算校验码
+ ///
+ /// 不含校验位的8位代码
+ /// 校验码,计算失败返回null
+ public static char? CalculateCheckCode(string? code8)
+ {
+ if (string.IsNullOrWhiteSpace(code8) || code8.Length != 8)
+ {
+ return null;
+ }
+
+ int sum = 0;
+ for (int i = 0; i < 8; i++)
+ {
+ char c = char.ToUpper(code8[i]);
+ int value;
+
+ if (c >= '0' && c <= '9')
+ {
+ value = c - '0';
+ }
+ else if (c >= 'A' && c <= 'Z')
+ {
+ value = c - 'A' + 10;
+ }
+ else
+ {
+ return null;
+ }
+
+ sum += value * CharWeights[i];
+ }
+
+ int checkIndex = 11 - (sum % 11);
+ if (checkIndex == 11)
+ {
+ checkIndex = 0;
+ }
+ else if (checkIndex == 10)
+ {
+ return 'X'; // 10对应X
+ }
+
+ return CheckCodes[checkIndex];
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取机构类型(第1位)
+ ///
+ /// 组织机构代码
+ /// 机构类型
+ public static string? GetOrganizationType(string? orgCode)
+ {
+ if (!IsValid(orgCode))
+ {
+ return null;
+ }
+
+ char typeCode = char.ToUpper(orgCode!.Replace("-", "")[0]);
+ return typeCode switch
+ {
+ '1' => "企业法人",
+ '2' => "企业非法人",
+ '3' => "事业法人",
+ '4' => "事业非法人",
+ '5' => "机关法人",
+ '6' => "机关非法人",
+ '7' => "社会团体法人",
+ '8' => "社会团体非法人",
+ '9' => "其他机构",
+ 'A' => "企业法人(外资)",
+ 'B' => "企业非法人(外资)",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取登记管理机关行政区划代码(第2-8位)
+ ///
+ /// 组织机构代码
+ /// 行政区划代码
+ public static string? GetAreaCode(string? orgCode)
+ {
+ if (!IsValid(orgCode))
+ {
+ return null;
+ }
+
+ string code = orgCode!.Replace("-", "");
+ return code.Substring(1, 7);
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化组织机构代码(XXXXXXXX-X)
+ ///
+ /// 组织机构代码
+ /// 格式化后的代码
+ public static string? Format(string? orgCode)
+ {
+ if (!IsValid(orgCode))
+ {
+ return null;
+ }
+
+ string code = orgCode!.ToUpper().Replace("-", "");
+ return code.Substring(0, 8) + "-" + code[8];
+ }
+
+ ///
+ /// 清理组织机构代码(去除分隔符)
+ ///
+ /// 组织机构代码
+ /// 清理后的代码
+ public static string? Normalize(string? orgCode)
+ {
+ if (string.IsNullOrWhiteSpace(orgCode))
+ {
+ return null;
+ }
+
+ string code = orgCode.ToUpper().Replace("-", "").Trim();
+ return code.Length == 9 && OrgCodeRegex.IsMatch(code) ? code : null;
+ }
+
+ ///
+ /// 组织机构代码脱敏:123****9X
+ ///
+ /// 组织机构代码
+ /// 脱敏后的代码
+ public static string? Mask(string? orgCode)
+ {
+ if (!IsValid(orgCode))
+ {
+ return null;
+ }
+
+ string code = orgCode!.Replace("-", "");
+ return code.Substring(0, 3) + "*****" + code.Substring(8);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机组织机构代码(仅供测试使用)
+ ///
+ /// 9位组织机构代码
+ public static string GenerateRandom()
+ {
+ const string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ // 生成前8位
+ string code8 = "";
+ for (int i = 0; i < 8; i++)
+ {
+ code8 += MathCategory.RandomUtil.GetRandomElement(chars.ToCharArray());
+ }
+
+ // 计算校验码
+ char? checkCode = CalculateCheckCode(code8);
+ return code8 + (checkCode ?? '0');
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/PassportUtil.cs b/EasyTool.Core/BusinessCategory/PassportUtil.cs
new file mode 100644
index 0000000..9ee432d
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/PassportUtil.cs
@@ -0,0 +1,375 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 护照类型枚举
+ ///
+ public enum PassportType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// 中国普通护照(E开头+8位数字)
+ ///
+ ChinaOrdinary = 1,
+
+ ///
+ /// 中国公务护照(SE开头+7位数字)
+ ///
+ ChinaService = 2,
+
+ ///
+ /// 中国外交护照(DE开头+7位数字)
+ ///
+ ChinaDiplomatic = 3,
+
+ ///
+ /// 中国香港特区护照
+ ///
+ HongKong = 4,
+
+ ///
+ /// 中国澳门特区护照
+ ///
+ Macau = 5,
+
+ ///
+ /// 台湾护照
+ ///
+ Taiwan = 6
+ }
+
+ ///
+ /// 护照号工具类
+ ///
+ public static class PassportUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 中国普通护照正则(E+8位数字)
+ ///
+ private static readonly Regex ChinaOrdinaryRegex = new Regex(@"^[Ee]\d{8}$", RegexOptions.Compiled);
+
+ ///
+ /// 中国公务护照正则(SE+7位数字)
+ ///
+ private static readonly Regex ChinaServiceRegex = new Regex(@"^[Ss][Ee]\d{7}$", RegexOptions.Compiled);
+
+ ///
+ /// 中国外交护照正则(DE+7位数字)
+ ///
+ private static readonly Regex ChinaDiplomaticRegex = new Regex(@"^[Dd][Ee]\d{7}$", RegexOptions.Compiled);
+
+ ///
+ /// 中国香港护照正则(K+8位数字 或 881/159开头+7位数字)
+ ///
+ private static readonly Regex HongKongRegex = new Regex(@"^([Kk]\d{8}|(881|159)\d{7})$", RegexOptions.Compiled);
+
+ ///
+ /// 中国澳门护照正则(578开头+7位数字 或 1+7位数字)
+ ///
+ private static readonly Regex MacauRegex = new Regex(@"^(578\d{7}|[1]\d{7})$", RegexOptions.Compiled);
+
+ ///
+ /// 台湾护照正则(数字+字母混合,9-10位)
+ ///
+ private static readonly Regex TaiwanRegex = new Regex(@"^\d{8,9}$|^[A-Za-z]\d{8,9}$", RegexOptions.Compiled);
+
+ ///
+ /// 通用护照号正则(2-3位字母+6-9位数字,或纯数字8-9位)
+ ///
+ private static readonly Regex GeneralPassportRegex = new Regex(
+ @"^([A-Za-z]{1,3}\d{6,9}|\d{8,9})$",
+ RegexOptions.Compiled);
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证护照号是否有效(自动识别类型)
+ ///
+ /// 护照号
+ /// 是否有效
+ public static bool IsValid(string? passportNumber)
+ {
+ return GetPassportType(passportNumber) != PassportType.Unknown;
+ }
+
+ ///
+ /// 验证中国普通护照号是否有效
+ ///
+ /// 护照号
+ /// 是否有效
+ public static bool IsChinaOrdinary(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return false;
+ }
+
+ return ChinaOrdinaryRegex.IsMatch(passportNumber);
+ }
+
+ ///
+ /// 验证中国公务护照号是否有效
+ ///
+ /// 护照号
+ /// 是否有效
+ public static bool IsChinaService(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return false;
+ }
+
+ return ChinaServiceRegex.IsMatch(passportNumber);
+ }
+
+ ///
+ /// 验证中国外交护照号是否有效
+ ///
+ /// 护照号
+ /// 是否有效
+ public static bool IsChinaDiplomatic(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return false;
+ }
+
+ return ChinaDiplomaticRegex.IsMatch(passportNumber);
+ }
+
+ ///
+ /// 验证中国香港护照号是否有效
+ ///
+ /// 护照号
+ /// 是否有效
+ public static bool IsHongKong(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return false;
+ }
+
+ return HongKongRegex.IsMatch(passportNumber);
+ }
+
+ ///
+ /// 验证中国澳门护照号是否有效
+ ///
+ /// 护照号
+ /// 是否有效
+ public static bool IsMacau(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return false;
+ }
+
+ return MacauRegex.IsMatch(passportNumber);
+ }
+
+ ///
+ /// 验证台湾护照号是否有效
+ ///
+ /// 护照号
+ /// 是否有效
+ public static bool IsTaiwan(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return false;
+ }
+
+ return TaiwanRegex.IsMatch(passportNumber);
+ }
+
+ ///
+ /// 验证是否为中国大陆护照(含普通、公务、外交)
+ ///
+ /// 护照号
+ /// 是否为中国大陆护照
+ public static bool IsChinaMainland(string? passportNumber)
+ {
+ return IsChinaOrdinary(passportNumber) ||
+ IsChinaService(passportNumber) ||
+ IsChinaDiplomatic(passportNumber);
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取护照类型
+ ///
+ /// 护照号
+ /// 护照类型
+ public static PassportType GetPassportType(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return PassportType.Unknown;
+ }
+
+ string upper = passportNumber.ToUpper();
+
+ // 中国普通护照
+ if (ChinaOrdinaryRegex.IsMatch(upper))
+ {
+ return PassportType.ChinaOrdinary;
+ }
+
+ // 中国公务护照
+ if (ChinaServiceRegex.IsMatch(upper))
+ {
+ return PassportType.ChinaService;
+ }
+
+ // 中国外交护照
+ if (ChinaDiplomaticRegex.IsMatch(upper))
+ {
+ return PassportType.ChinaDiplomatic;
+ }
+
+ // 香港护照
+ if (HongKongRegex.IsMatch(upper))
+ {
+ return PassportType.HongKong;
+ }
+
+ // 澳门护照
+ if (MacauRegex.IsMatch(upper))
+ {
+ return PassportType.Macau;
+ }
+
+ // 台湾护照
+ if (TaiwanRegex.IsMatch(upper))
+ {
+ return PassportType.Taiwan;
+ }
+
+ return PassportType.Unknown;
+ }
+
+ ///
+ /// 获取护照类型名称
+ ///
+ /// 护照号
+ /// 护照类型名称
+ public static string? GetPassportTypeName(string? passportNumber)
+ {
+ PassportType type = GetPassportType(passportNumber);
+ return type switch
+ {
+ PassportType.ChinaOrdinary => "中国普通护照",
+ PassportType.ChinaService => "中国公务护照",
+ PassportType.ChinaDiplomatic => "中国外交护照",
+ PassportType.HongKong => "香港特区护照",
+ PassportType.Macau => "澳门特区护照",
+ PassportType.Taiwan => "台湾护照",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取护照类型描述
+ ///
+ /// 护照类型
+ /// 类型描述
+ public static string GetTypeDescription(PassportType type)
+ {
+ return type switch
+ {
+ PassportType.ChinaOrdinary => "中国普通护照(E+8位数字)",
+ PassportType.ChinaService => "中国公务护照(SE+7位数字)",
+ PassportType.ChinaDiplomatic => "中国外交护照(DE+7位数字)",
+ PassportType.HongKong => "香港特区护照(K+8位数字)",
+ PassportType.Macau => "澳门特区护照(578开头+7位数字)",
+ PassportType.Taiwan => "台湾护照(8-9位数字)",
+ _ => "未知类型"
+ };
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化护照号(转大写,去除空格和特殊字符)
+ ///
+ /// 护照号
+ /// 格式化后的护照号
+ public static string? Normalize(string? passportNumber)
+ {
+ if (string.IsNullOrWhiteSpace(passportNumber))
+ {
+ return null;
+ }
+
+ // 去除空格和特殊字符,转大写
+ string normalized = passportNumber.ToUpper().Trim();
+ normalized = Regex.Replace(normalized, @"[^A-Z0-9]", "");
+
+ return normalized;
+ }
+
+ ///
+ /// 护照号脱敏:E********(保留首字母)
+ ///
+ /// 护照号
+ /// 脱敏后的护照号
+ public static string? Mask(string? passportNumber)
+ {
+ string? normalized = Normalize(passportNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ // 保留首字符,其余用*代替
+ if (normalized.Length <= 2)
+ {
+ return normalized[0] + "*";
+ }
+
+ // 保留前2位和后2位
+ return normalized.Substring(0, 2) + new string('*', normalized.Length - 4) + normalized.Substring(normalized.Length - 2);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机护照号(仅供测试使用)
+ ///
+ /// 护照类型(可选,默认中国普通护照)
+ /// 护照号
+ public static string GenerateRandom(PassportType type = PassportType.ChinaOrdinary)
+ {
+ return type switch
+ {
+ PassportType.ChinaOrdinary => "E" + MathCategory.RandomUtil.RandomDigitString(8),
+ PassportType.ChinaService => "SE" + MathCategory.RandomUtil.RandomDigitString(7),
+ PassportType.ChinaDiplomatic => "DE" + MathCategory.RandomUtil.RandomDigitString(7),
+ PassportType.HongKong => "K" + MathCategory.RandomUtil.RandomDigitString(8),
+ PassportType.Macau => "578" + MathCategory.RandomUtil.RandomDigitString(7),
+ PassportType.Taiwan => MathCategory.RandomUtil.RandomDigitString(9),
+ _ => "E" + MathCategory.RandomUtil.RandomDigitString(8)
+ };
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/PasswordUtil.cs b/EasyTool.Core/BusinessCategory/PasswordUtil.cs
new file mode 100644
index 0000000..c3e66ed
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/PasswordUtil.cs
@@ -0,0 +1,586 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 密码强度等级
+ ///
+ public enum PasswordStrength
+ {
+ ///
+ /// 非常弱
+ ///
+ VeryWeak = 0,
+
+ ///
+ /// 弱
+ ///
+ Weak = 1,
+
+ ///
+ /// 中等
+ ///
+ Medium = 2,
+
+ ///
+ /// 强
+ ///
+ Strong = 3,
+
+ ///
+ /// 非常强
+ ///
+ VeryStrong = 4
+ }
+
+ ///
+ /// 密码验证结果
+ ///
+ public class PasswordValidationResult
+ {
+ ///
+ /// 是否有效
+ ///
+ public bool IsValid { get; set; }
+
+ ///
+ /// 密码强度
+ ///
+ public PasswordStrength Strength { get; set; }
+
+ ///
+ /// 强度分数(0-100)
+ ///
+ public int Score { get; set; }
+
+ ///
+ /// 错误信息列表
+ ///
+ public List Errors { get; set; } = new List();
+
+ ///
+ /// 警告信息列表
+ ///
+ public List Warnings { get; set; } = new List();
+ }
+
+ ///
+ /// 密码验证选项
+ ///
+ public class PasswordValidationOptions
+ {
+ ///
+ /// 最小长度(默认8)
+ ///
+ public int MinLength { get; set; } = 8;
+
+ ///
+ /// 最大长度(默认128)
+ ///
+ public int MaxLength { get; set; } = 128;
+
+ ///
+ /// 是否要求包含小写字母(默认true)
+ ///
+ public bool RequireLowercase { get; set; } = true;
+
+ ///
+ /// 是否要求包含大写字母(默认true)
+ ///
+ public bool RequireUppercase { get; set; } = true;
+
+ ///
+ /// 是否要求包含数字(默认true)
+ ///
+ public bool RequireDigit { get; set; } = true;
+
+ ///
+ /// 是否要求包含特殊字符(默认true)
+ ///
+ public bool RequireSpecialChar { get; set; } = true;
+
+ ///
+ /// 允许的特殊字符(默认!@#$%^&*()_+-=[]{}|;:',.<>?)
+ ///
+ public string AllowedSpecialChars { get; set; } = "!@#$%^&*()_+-=[]{}|;:',.<>?";
+
+ ///
+ /// 最少不同字符类型数量(默认3)
+ ///
+ public int MinCharacterTypes { get; set; } = 3;
+
+ ///
+ /// 是否禁止常见弱密码(默认true)
+ ///
+ public bool BlockCommonPasswords { get; set; } = true;
+
+ ///
+ /// 是否禁止连续重复字符(默认true)
+ ///
+ public bool BlockRepeatingChars { get; set; } = true;
+
+ ///
+ /// 是否禁止连续递增/递减字符(如123、abc)(默认true)
+ ///
+ public bool BlockSequentialChars { get; set; } = true;
+ }
+
+ ///
+ /// 密码工具类
+ ///
+ public static class PasswordUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 常见弱密码列表
+ ///
+ private static readonly HashSet CommonPasswords = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "password", "123456", "12345678", "123456789", "1234567890",
+ "qwerty", "abc123", "password123", "admin", "admin123",
+ "root", "root123", "111111", "000000", "123123",
+ "password1", "iloveyou", "monkey", "dragon", "master",
+ "letmein", "login", "welcome", "shadow", "sunshine",
+ "princess", "football", "baseball", "soccer", "hockey",
+ "batman", "superman", "trustno1", "passw0rd", "qazwsx",
+ "qwerty123", "123qwe", "654321", "888888", "666666"
+ };
+
+ ///
+ /// 键盘连续字符模式
+ ///
+ private static readonly string[] KeyboardSequences = {
+ "qwertyuiop", "asdfghjkl", "zxcvbnm",
+ "qwertyuiop".ToUpper(), "asdfghjkl".ToUpper(), "zxcvbnm".ToUpper()
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证密码(使用默认选项)
+ ///
+ /// 密码
+ /// 验证结果
+ public static PasswordValidationResult Validate(string? password)
+ {
+ return Validate(password, new PasswordValidationOptions());
+ }
+
+ ///
+ /// 验证密码(使用自定义选项)
+ ///
+ /// 密码
+ /// 验证选项
+ /// 验证结果
+ public static PasswordValidationResult Validate(string? password, PasswordValidationOptions options)
+ {
+ var result = new PasswordValidationResult();
+
+ // 空值检查
+ if (string.IsNullOrEmpty(password))
+ {
+ result.IsValid = false;
+ result.Errors.Add("密码不能为空");
+ result.Score = 0;
+ result.Strength = PasswordStrength.VeryWeak;
+ return result;
+ }
+
+ // 长度检查
+ if (password.Length < options.MinLength)
+ {
+ result.Errors.Add($"密码长度不能少于{options.MinLength}位");
+ }
+
+ if (password.Length > options.MaxLength)
+ {
+ result.Errors.Add($"密码长度不能超过{options.MaxLength}位");
+ }
+
+ // 字符类型检查
+ bool hasLowercase = Regex.IsMatch(password, @"[a-z]");
+ bool hasUppercase = Regex.IsMatch(password, @"[A-Z]");
+ bool hasDigit = Regex.IsMatch(password, @"\d");
+ bool hasSpecial = Regex.IsMatch(password, @"[!@#$%^&*()_+\-=\[\]{}|;:',.<>?]");
+
+ if (options.RequireLowercase && !hasLowercase)
+ {
+ result.Errors.Add("密码必须包含小写字母");
+ }
+
+ if (options.RequireUppercase && !hasUppercase)
+ {
+ result.Errors.Add("密码必须包含大写字母");
+ }
+
+ if (options.RequireDigit && !hasDigit)
+ {
+ result.Errors.Add("密码必须包含数字");
+ }
+
+ if (options.RequireSpecialChar && !hasSpecial)
+ {
+ result.Errors.Add("密码必须包含特殊字符");
+ }
+
+ // 统计字符类型数量
+ int charTypeCount = (hasLowercase ? 1 : 0) + (hasUppercase ? 1 : 0) + (hasDigit ? 1 : 0) + (hasSpecial ? 1 : 0);
+ if (charTypeCount < options.MinCharacterTypes)
+ {
+ result.Errors.Add($"密码必须包含至少{options.MinCharacterTypes}种不同类型的字符");
+ }
+
+ // 检查非法字符
+ if (!string.IsNullOrEmpty(options.AllowedSpecialChars))
+ {
+ string allowedPattern = $@"^[a-zA-Z0-9{Regex.Escape(options.AllowedSpecialChars)}]+$";
+ if (!Regex.IsMatch(password, allowedPattern))
+ {
+ result.Errors.Add("密码包含非法字符");
+ }
+ }
+
+ // 检查常见弱密码
+ if (options.BlockCommonPasswords && CommonPasswords.Contains(password))
+ {
+ result.Errors.Add("密码过于简单,请使用更复杂的密码");
+ }
+
+ // 检查连续重复字符
+ if (options.BlockRepeatingChars && HasRepeatingChars(password, 3))
+ {
+ result.Warnings.Add("密码包含连续重复的字符");
+ }
+
+ // 检查连续递增/递减字符
+ if (options.BlockSequentialChars && HasSequentialChars(password))
+ {
+ result.Warnings.Add("密码包含连续的递增或递减字符");
+ }
+
+ // 计算强度分数
+ int score = CalculateScore(password, hasLowercase, hasUppercase, hasDigit, hasSpecial);
+ result.Score = score;
+ result.Strength = GetStrengthFromScore(score);
+
+ // 确定是否有效
+ result.IsValid = result.Errors.Count == 0;
+
+ return result;
+ }
+
+ ///
+ /// 快速验证密码是否符合基本要求
+ ///
+ /// 密码
+ /// 最小长度(默认8)
+ /// 是否有效
+ public static bool IsValid(string? password, int minLength = 8)
+ {
+ if (string.IsNullOrEmpty(password) || password.Length < minLength)
+ {
+ return false;
+ }
+
+ bool hasLower = Regex.IsMatch(password, @"[a-z]");
+ bool hasUpper = Regex.IsMatch(password, @"[A-Z]");
+ bool hasDigit = Regex.IsMatch(password, @"\d");
+
+ return hasLower && hasUpper && hasDigit;
+ }
+
+ #endregion
+
+ #region 强度评估
+
+ ///
+ /// 评估密码强度
+ ///
+ /// 密码
+ /// 密码强度
+ public static PasswordStrength EvaluateStrength(string? password)
+ {
+ if (string.IsNullOrEmpty(password))
+ {
+ return PasswordStrength.VeryWeak;
+ }
+
+ bool hasLower = Regex.IsMatch(password, @"[a-z]");
+ bool hasUpper = Regex.IsMatch(password, @"[A-Z]");
+ bool hasDigit = Regex.IsMatch(password, @"\d");
+ bool hasSpecial = Regex.IsMatch(password, @"[!@#$%^&*()_+\-=\[\]{}|;:',.<>?]");
+
+ int score = CalculateScore(password, hasLower, hasUpper, hasDigit, hasSpecial);
+ return GetStrengthFromScore(score);
+ }
+
+ ///
+ /// 获取密码强度分数(0-100)
+ ///
+ /// 密码
+ /// 强度分数
+ public static int GetStrengthScore(string? password)
+ {
+ if (string.IsNullOrEmpty(password))
+ {
+ return 0;
+ }
+
+ bool hasLower = Regex.IsMatch(password, @"[a-z]");
+ bool hasUpper = Regex.IsMatch(password, @"[A-Z]");
+ bool hasDigit = Regex.IsMatch(password, @"\d");
+ bool hasSpecial = Regex.IsMatch(password, @"[!@#$%^&*()_+\-=\[\]{}|;:',.<>?]");
+
+ return CalculateScore(password, hasLower, hasUpper, hasDigit, hasSpecial);
+ }
+
+ ///
+ /// 获取密码强度描述
+ ///
+ /// 密码强度
+ /// 强度描述
+ public static string GetStrengthDescription(PasswordStrength strength)
+ {
+ return strength switch
+ {
+ PasswordStrength.VeryWeak => "非常弱",
+ PasswordStrength.Weak => "弱",
+ PasswordStrength.Medium => "中等",
+ PasswordStrength.Strong => "强",
+ PasswordStrength.VeryStrong => "非常强",
+ _ => "未知"
+ };
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机密码
+ ///
+ /// 密码长度(默认12)
+ /// 包含小写字母(默认true)
+ /// 包含大写字母(默认true)
+ /// 包含数字(默认true)
+ /// 包含特殊字符(默认true)
+ /// 随机密码
+ public static string GenerateRandom(
+ int length = 12,
+ bool includeLowercase = true,
+ bool includeUppercase = true,
+ bool includeDigits = true,
+ bool includeSpecialChars = true)
+ {
+ const string lowercase = "abcdefghijklmnopqrstuvwxyz";
+ const string uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ const string digits = "0123456789";
+ const string special = "!@#$%^&*()_+-=[]{}|;:',.<>?";
+
+ string charSet = "";
+ if (includeLowercase) charSet += lowercase;
+ if (includeUppercase) charSet += uppercase;
+ if (includeDigits) charSet += digits;
+ if (includeSpecialChars) charSet += special;
+
+ if (string.IsNullOrEmpty(charSet))
+ {
+ charSet = lowercase + digits;
+ }
+
+ string password = "";
+ for (int i = 0; i < length; i++)
+ {
+ password += MathCategory.RandomUtil.GetRandomElement(charSet.ToCharArray());
+ }
+
+ return password;
+ }
+
+ ///
+ /// 生成强密码(确保包含所有字符类型)
+ ///
+ /// 密码长度(默认16)
+ /// 强密码
+ public static string GenerateStrong(int length = 16)
+ {
+ const string lowercase = "abcdefghijklmnopqrstuvwxyz";
+ const string uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ const string digits = "0123456789";
+ const string special = "!@#$%^&*()_+-=";
+
+ if (length < 4)
+ {
+ length = 4;
+ }
+
+ // 确保每种字符类型至少有一个
+ var password = new List();
+ password.Add(MathCategory.RandomUtil.GetRandomElement(lowercase.ToCharArray()));
+ password.Add(MathCategory.RandomUtil.GetRandomElement(uppercase.ToCharArray()));
+ password.Add(MathCategory.RandomUtil.GetRandomElement(digits.ToCharArray()));
+ password.Add(MathCategory.RandomUtil.GetRandomElement(special.ToCharArray()));
+
+ // 填充剩余字符
+ string allChars = lowercase + uppercase + digits + special;
+ for (int i = 4; i < length; i++)
+ {
+ password.Add(MathCategory.RandomUtil.GetRandomElement(allChars.ToCharArray()));
+ }
+
+ // 随机打乱顺序
+ for (int i = password.Count - 1; i > 0; i--)
+ {
+ int j = MathCategory.RandomUtil.RandomInt(0, i + 1);
+ (password[i], password[j]) = (password[j], password[i]);
+ }
+
+ return new string(password.ToArray());
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 计算密码强度分数
+ ///
+ private static int CalculateScore(string password, bool hasLower, bool hasUpper, bool hasDigit, bool hasSpecial)
+ {
+ int score = 0;
+
+ // 长度分数(最多40分)
+ score += Math.Min(password.Length * 4, 40);
+
+ // 字符类型分数(每种类型10分,最多40分)
+ if (hasLower) score += 10;
+ if (hasUpper) score += 10;
+ if (hasDigit) score += 10;
+ if (hasSpecial) score += 10;
+
+ // 混合奖励(最多10分)
+ int typeCount = (hasLower ? 1 : 0) + (hasUpper ? 1 : 0) + (hasDigit ? 1 : 0) + (hasSpecial ? 1 : 0);
+ if (typeCount >= 3) score += 5;
+ if (typeCount == 4) score += 5;
+
+ // 惩罚
+ // 常见弱密码
+ if (CommonPasswords.Contains(password))
+ {
+ score = Math.Max(0, score - 30);
+ }
+
+ // 全部相同字符
+ if (AllSameChar(password))
+ {
+ score = Math.Max(0, score - 20);
+ }
+
+ // 连续字符
+ if (HasSequentialChars(password))
+ {
+ score = Math.Max(0, score - 10);
+ }
+
+ return Math.Min(100, Math.Max(0, score));
+ }
+
+ ///
+ /// 根据分数获取强度等级
+ ///
+ private static PasswordStrength GetStrengthFromScore(int score)
+ {
+ if (score < 20) return PasswordStrength.VeryWeak;
+ if (score < 40) return PasswordStrength.Weak;
+ if (score < 60) return PasswordStrength.Medium;
+ if (score < 80) return PasswordStrength.Strong;
+ return PasswordStrength.VeryStrong;
+ }
+
+ ///
+ /// 检查是否所有字符相同
+ ///
+ private static bool AllSameChar(string str)
+ {
+ if (string.IsNullOrEmpty(str)) return true;
+ char first = str[0];
+ foreach (char c in str)
+ {
+ if (c != first) return false;
+ }
+ return true;
+ }
+
+ ///
+ /// 检查是否有连续重复字符
+ ///
+ private static bool HasRepeatingChars(string str, int count)
+ {
+ if (string.IsNullOrEmpty(str) || str.Length < count) return false;
+
+ for (int i = 0; i <= str.Length - count; i++)
+ {
+ bool allSame = true;
+ for (int j = 1; j < count; j++)
+ {
+ if (str[i + j] != str[i])
+ {
+ allSame = false;
+ break;
+ }
+ }
+ if (allSame) return true;
+ }
+ return false;
+ }
+
+ ///
+ /// 检查是否有连续递增/递减字符
+ ///
+ private static bool HasSequentialChars(string str)
+ {
+ if (string.IsNullOrEmpty(str) || str.Length < 3) return false;
+
+ string lower = str.ToLower();
+
+ // 检查字母和数字序列
+ for (int i = 0; i <= lower.Length - 3; i++)
+ {
+ // 检查递增
+ if (lower[i + 1] == lower[i] + 1 && lower[i + 2] == lower[i] + 2)
+ {
+ return true;
+ }
+ // 检查递减
+ if (lower[i + 1] == lower[i] - 1 && lower[i + 2] == lower[i] - 2)
+ {
+ return true;
+ }
+ }
+
+ // 检查键盘序列
+ foreach (string seq in KeyboardSequences)
+ {
+ if (seq.Contains(lower.Substring(0, Math.Min(3, lower.Length))))
+ {
+ for (int i = 0; i <= lower.Length - 3; i++)
+ {
+ if (seq.Contains(lower.Substring(i, 3)))
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/PhoneNumberUtil.cs b/EasyTool.Core/BusinessCategory/PhoneNumberUtil.cs
new file mode 100644
index 0000000..175383a
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/PhoneNumberUtil.cs
@@ -0,0 +1,350 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 手机运营商枚举
+ ///
+ public enum Carrier
+ {
+ ///
+ /// 未知运营商
+ ///
+ Unknown = 0,
+
+ ///
+ /// 中国移动
+ ///
+ ChinaMobile = 1,
+
+ ///
+ /// 中国联通
+ ///
+ ChinaUnicom = 2,
+
+ ///
+ /// 中国电信
+ ///
+ ChinaTelecom = 3,
+
+ ///
+ /// 中国广电
+ ///
+ ChinaBroadnet = 4
+ }
+
+ ///
+ /// 手机号工具类
+ ///
+ public static class PhoneNumberUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 手机号正则表达式(11位,1开头)
+ ///
+ private static readonly Regex PhoneRegex = new Regex(@"^1[3-9]\d{9}$", RegexOptions.Compiled);
+
+ ///
+ /// 中国移动号段(前3-4位)
+ ///
+ private static readonly HashSet ChinaMobilePrefixes = new HashSet
+ {
+ "134", "135", "136", "137", "138", "139", "147", "150", "151", "152",
+ "157", "158", "159", "172", "178", "182", "183", "184", "187", "188",
+ "195", "197", "198"
+ };
+
+ ///
+ /// 中国联通号段(前3-4位)
+ ///
+ private static readonly HashSet ChinaUnicomPrefixes = new HashSet
+ {
+ "130", "131", "132", "145", "155", "156", "166", "167", "175", "176",
+ "185", "186", "196"
+ };
+
+ ///
+ /// 中国电信号段(前3-4位)
+ ///
+ private static readonly HashSet ChinaTelecomPrefixes = new HashSet
+ {
+ "133", "149", "153", "173", "174", "177", "180", "181", "189", "191",
+ "193", "199"
+ };
+
+ ///
+ /// 中国广电号段(前3-4位)
+ ///
+ private static readonly HashSet ChinaBroadnetPrefixes = new HashSet
+ {
+ "192"
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证手机号格式是否有效
+ ///
+ /// 手机号
+ /// 是否有效
+ public static bool IsValid(string? phoneNumber)
+ {
+ if (string.IsNullOrWhiteSpace(phoneNumber))
+ {
+ return false;
+ }
+
+ return PhoneRegex.IsMatch(phoneNumber);
+ }
+
+ ///
+ /// 格式化并验证手机号(去除非数字字符后验证)
+ ///
+ /// 手机号
+ /// 格式化后的手机号,无效返回null
+ public static string? Normalize(string? phoneNumber)
+ {
+ if (string.IsNullOrWhiteSpace(phoneNumber))
+ {
+ return null;
+ }
+
+ // 去除所有非数字字符
+ string normalized = Regex.Replace(phoneNumber, @"\D", "");
+
+ if (!IsValid(normalized))
+ {
+ return null;
+ }
+
+ return normalized;
+ }
+
+ #endregion
+
+ #region 运营商识别
+
+ ///
+ /// 获取运营商枚举
+ ///
+ /// 手机号
+ /// 运营商枚举
+ public static Carrier GetCarrier(string? phoneNumber)
+ {
+ if (!IsValid(phoneNumber))
+ {
+ return Carrier.Unknown;
+ }
+
+ string prefix3 = phoneNumber!.Substring(0, 3);
+
+ if (ChinaMobilePrefixes.Contains(prefix3))
+ {
+ return Carrier.ChinaMobile;
+ }
+
+ if (ChinaUnicomPrefixes.Contains(prefix3))
+ {
+ return Carrier.ChinaUnicom;
+ }
+
+ if (ChinaTelecomPrefixes.Contains(prefix3))
+ {
+ return Carrier.ChinaTelecom;
+ }
+
+ if (ChinaBroadnetPrefixes.Contains(prefix3))
+ {
+ return Carrier.ChinaBroadnet;
+ }
+
+ return Carrier.Unknown;
+ }
+
+ ///
+ /// 获取运营商名称
+ ///
+ /// 手机号
+ /// 运营商名称
+ public static string? GetCarrierName(string? phoneNumber)
+ {
+ Carrier carrier = GetCarrier(phoneNumber);
+ return carrier switch
+ {
+ Carrier.ChinaMobile => "中国移动",
+ Carrier.ChinaUnicom => "中国联通",
+ Carrier.ChinaTelecom => "中国电信",
+ Carrier.ChinaBroadnet => "中国广电",
+ _ => null
+ };
+ }
+
+ ///
+ /// 判断是否为中国移动号码
+ ///
+ /// 手机号
+ /// 是否为移动号码
+ public static bool IsChinaMobile(string? phoneNumber)
+ {
+ return GetCarrier(phoneNumber) == Carrier.ChinaMobile;
+ }
+
+ ///
+ /// 判断是否为中国联通号码
+ ///
+ /// 手机号
+ /// 是否为联通号码
+ public static bool IsChinaUnicom(string? phoneNumber)
+ {
+ return GetCarrier(phoneNumber) == Carrier.ChinaUnicom;
+ }
+
+ ///
+ /// 判断是否为中国电信号码
+ ///
+ /// 手机号
+ /// 是否为电信号码
+ public static bool IsChinaTelecom(string? phoneNumber)
+ {
+ return GetCarrier(phoneNumber) == Carrier.ChinaTelecom;
+ }
+
+ ///
+ /// 判断是否为中国广电号码
+ ///
+ /// 手机号
+ /// 是否为广电号码
+ public static bool IsChinaBroadnet(string? phoneNumber)
+ {
+ return GetCarrier(phoneNumber) == Carrier.ChinaBroadnet;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化手机号(空格分隔):138 8888 8888
+ ///
+ /// 手机号
+ /// 格式化后的手机号
+ public static string? FormatWithSpaces(string? phoneNumber)
+ {
+ string? normalized = Normalize(phoneNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ return $"{normalized.Substring(0, 3)} {normalized.Substring(3, 4)} {normalized.Substring(7, 4)}";
+ }
+
+ ///
+ /// 格式化手机号(横线分隔):138-8888-8888
+ ///
+ /// 手机号
+ /// 格式化后的手机号
+ public static string? FormatWithHyphens(string? phoneNumber)
+ {
+ string? normalized = Normalize(phoneNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ return $"{normalized.Substring(0, 3)}-{normalized.Substring(3, 4)}-{normalized.Substring(7, 4)}";
+ }
+
+ ///
+ /// 格式化手机号(带国际区号):+86 13888888888
+ ///
+ /// 手机号
+ /// 格式化后的手机号
+ public static string? FormatWithCountryCode(string? phoneNumber)
+ {
+ string? normalized = Normalize(phoneNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ return $"+86 {normalized}";
+ }
+
+ ///
+ /// 手机号脱敏:138****8888
+ ///
+ /// 手机号
+ /// 脱敏后的手机号
+ public static string? Mask(string? phoneNumber)
+ {
+ string? normalized = Normalize(phoneNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ return $"{normalized.Substring(0, 3)}****{normalized.Substring(7, 4)}";
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机手机号(仅供测试使用)
+ ///
+ /// 运营商(可选,默认随机)
+ /// 11位手机号
+ public static string GenerateRandom(Carrier? carrier = null)
+ {
+ string prefix;
+
+ if (carrier.HasValue && carrier.Value != Carrier.Unknown)
+ {
+ prefix = carrier.Value switch
+ {
+ Carrier.ChinaMobile => MathCategory.RandomUtil.GetRandomElement(ChinaMobilePrefixes),
+ Carrier.ChinaUnicom => MathCategory.RandomUtil.GetRandomElement(ChinaUnicomPrefixes),
+ Carrier.ChinaTelecom => MathCategory.RandomUtil.GetRandomElement(ChinaTelecomPrefixes),
+ Carrier.ChinaBroadnet => MathCategory.RandomUtil.GetRandomElement(ChinaBroadnetPrefixes),
+ _ => GetRandomPrefix()
+ };
+ }
+ else
+ {
+ prefix = GetRandomPrefix();
+ }
+
+ // 生成剩余8位数字
+ string suffix = MathCategory.RandomUtil.RandomDigitString(8);
+
+ return prefix + suffix;
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 获取随机号段前缀
+ ///
+ private static string GetRandomPrefix()
+ {
+ var allPrefixes = new List();
+ allPrefixes.AddRange(ChinaMobilePrefixes);
+ allPrefixes.AddRange(ChinaUnicomPrefixes);
+ allPrefixes.AddRange(ChinaTelecomPrefixes);
+ allPrefixes.AddRange(ChinaBroadnetPrefixes);
+
+ return MathCategory.RandomUtil.GetRandomElement(allPrefixes);
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/PhoneUtil.cs b/EasyTool.Core/BusinessCategory/PhoneUtil.cs
new file mode 100644
index 0000000..3a2142f
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/PhoneUtil.cs
@@ -0,0 +1,404 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 固定电话工具类
+ ///
+ public static class PhoneUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 中国大陆固定电话正则表达式(带区号)
+ ///
+ private static readonly Regex PhoneWithAreaCodeRegex = new(
+ @"^(0\d{2,3}[-\s]?)?\d{7,8}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 中国大陆固定电话正则表达式(完整格式)
+ ///
+ private static readonly Regex PhoneFullRegex = new(
+ @"^0\d{2,3}[-\s]?\d{7,8}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 400电话正则表达式
+ ///
+ private static readonly Regex Phone400Regex = new(
+ @"^400[-\s]?\d{3}[-\s]?\d{4}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 800电话正则表达式
+ ///
+ private static readonly Regex Phone800Regex = new(
+ @"^800[-\s]?\d{3}[-\s]?\d{4}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 区号与城市映射
+ ///
+ private static readonly Dictionary AreaCodeMap = new()
+ {
+ // 直辖市
+ { "010", "北京" }, { "021", "上海" }, { "022", "天津" }, { "023", "重庆" },
+
+ // 省会城市
+ { "0311", "石家庄" }, { "0351", "太原" }, { "0471", "呼和浩特" },
+ { "024", "沈阳" }, { "0431", "长春" }, { "0451", "哈尔滨" },
+ { "025", "南京" }, { "0571", "杭州" }, { "0551", "合肥" },
+ { "0591", "福州" }, { "0791", "南昌" }, { "0531", "济南" },
+ { "0371", "郑州" }, { "027", "武汉" }, { "0731", "长沙" },
+ { "020", "广州" }, { "0771", "南宁" }, { "0898", "海口" },
+ { "028", "成都" }, { "0851", "贵阳" }, { "0871", "昆明" },
+ { "0891", "拉萨" }, { "029", "西安" }, { "0931", "兰州" },
+ { "0971", "西宁" }, { "0951", "银川" }, { "0991", "乌鲁木齐" },
+
+ // 重要城市
+ { "0755", "深圳" }, { "0756", "珠海" }, { "0754", "汕头" },
+ { "0757", "佛山" }, { "0769", "东莞" }, { "0760", "中山" },
+ { "0512", "苏州" }, { "0510", "无锡" }, { "0574", "宁波" },
+ { "0577", "温州" }, { "0532", "青岛" }, { "0411", "大连" },
+ { "0592", "厦门" }, { "0514", "扬州" }, { "0519", "常州" },
+ { "0573", "嘉兴" }, { "0575", "绍兴" }, { "0576", "台州" },
+ { "0579", "金华" }, { "0752", "惠州" }, { "0753", "梅州" },
+ { "0758", "肇庆" }, { "0759", "湛江" }, { "0762", "河源" },
+ { "0763", "清远" }, { "0766", "云浮" }, { "0768", "潮州" },
+ { "0773", "桂林" }, { "0774", "梧州" }, { "0775", "玉林" },
+ { "0779", "北海" }, { "0772", "柳州" }, { "0778", "河池" },
+ { "0733", "株洲" }, { "0734", "衡阳" }, { "0735", "郴州" },
+ { "0737", "益阳" }, { "0738", "娄底" }, { "0739", "邵阳" },
+ { "0792", "九江" }, { "0793", "上饶" }, { "0795", "宜春" },
+ { "0796", "吉安" }, { "0797", "赣州" }, { "0799", "萍乡" },
+ { "0533", "淄博" }, { "0534", "德州" }, { "0535", "烟台" },
+ { "0536", "潍坊" }, { "0537", "济宁" }, { "0538", "泰安" },
+ { "0539", "临沂" }, { "0543", "滨州" }, { "0546", "东营" },
+ { "0379", "洛阳" }, { "0378", "开封" }, { "0372", "安阳" },
+ { "0373", "新乡" }, { "0374", "许昌" }, { "0375", "平顶山" },
+ { "0370", "商丘" }, { "0391", "焦作" }, { "0393", "濮阳" },
+ { "0395", "漯河" }, { "0396", "驻马店" }, { "0398", "三门峡" },
+ { "0376", "信阳" }, { "0377", "南阳" }, { "0392", "鹤壁" },
+ { "027", "武汉" }, { "0710", "襄阳" }, { "0711", "鄂州" },
+ { "0712", "孝感" }, { "0713", "黄冈" }, { "0714", "黄石" },
+ { "0715", "咸宁" }, { "0716", "荆州" }, { "0717", "宜昌" },
+ { "0718", "恩施" }, { "0719", "十堰" }, { "0722", "随州" },
+ { "0724", "荆门" }, { "0728", "仙桃" }, { "0730", "岳阳" },
+
+ // 三位区号
+ { "0310", "邯郸" }, { "0312", "保定" }, { "0313", "张家口" },
+ { "0314", "承德" }, { "0315", "唐山" }, { "0316", "廊坊" },
+ { "0317", "沧州" }, { "0318", "衡水" }, { "0319", "邢台" },
+ { "0335", "秦皇岛" }, { "0349", "朔州" }, { "0350", "忻州" },
+ { "0352", "大同" }, { "0353", "阳泉" }, { "0354", "晋中" },
+ { "0355", "长治" }, { "0356", "晋城" }, { "0357", "临汾" },
+ { "0358", "吕梁" }, { "0359", "运城" }, { "0410", "铁岭" },
+ { "0412", "鞍山" }, { "0413", "抚顺" }, { "0414", "本溪" },
+ { "0415", "丹东" }, { "0416", "锦州" }, { "0417", "营口" },
+ { "0418", "阜新" }, { "0419", "辽阳" }, { "0421", "朝阳" },
+ { "0427", "盘锦" }, { "0429", "葫芦岛" }, { "0432", "吉林市" },
+ { "0433", "延边" }, { "0434", "四平" }, { "0435", "通化" },
+ { "0436", "白城" }, { "0437", "辽源" }, { "0439", "白山" },
+ { "0438", "松原" }, { "0452", "齐齐哈尔" }, { "0453", "牡丹江" },
+ { "0454", "佳木斯" }, { "0455", "绥化" }, { "0456", "黑河" },
+ { "0457", "大兴安岭" }, { "0458", "伊春" }, { "0459", "大庆" },
+ { "0464", "七台河" }, { "0467", "鸡西" }, { "0468", "鹤岗" },
+ { "0469", "双鸭山" }, { "0470", "呼伦贝尔" }, { "0472", "包头" },
+ { "0473", "乌海" }, { "0474", "乌兰察布" }, { "0475", "通辽" },
+ { "0476", "赤峰" }, { "0477", "鄂尔多斯" }, { "0478", "巴彦淖尔" },
+ { "0479", "锡林郭勒" }, { "0482", "兴安盟" }, { "0483", "阿拉善" }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证固定电话是否有效
+ ///
+ /// 固定电话号码
+ /// 是否有效
+ public static bool IsValid(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ {
+ return false;
+ }
+
+ return PhoneWithAreaCodeRegex.IsMatch(phone) ||
+ Is400Phone(phone) || Is800Phone(phone);
+ }
+
+ ///
+ /// 验证是否为带区号的固定电话
+ ///
+ /// 电话号码
+ /// 是否为固定电话
+ public static bool IsLandline(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ {
+ return false;
+ }
+
+ return PhoneFullRegex.IsMatch(phone);
+ }
+
+ ///
+ /// 验证是否为400电话
+ ///
+ /// 电话号码
+ /// 是否为400电话
+ public static bool Is400Phone(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ {
+ return false;
+ }
+
+ return Phone400Regex.IsMatch(phone);
+ }
+
+ ///
+ /// 验证是否为800电话
+ ///
+ /// 电话号码
+ /// 是否为800电话
+ public static bool Is800Phone(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ {
+ return false;
+ }
+
+ return Phone800Regex.IsMatch(phone);
+ }
+
+ ///
+ /// 验证区号是否有效
+ ///
+ /// 区号
+ /// 是否有效
+ public static bool IsValidAreaCode(string? areaCode)
+ {
+ if (string.IsNullOrWhiteSpace(areaCode))
+ {
+ return false;
+ }
+
+ string code = areaCode.TrimStart('0');
+ return AreaCodeMap.ContainsKey("0" + code) || AreaCodeMap.ContainsKey(areaCode);
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取区号
+ ///
+ /// 电话号码
+ /// 区号
+ public static string? GetAreaCode(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ {
+ return null;
+ }
+
+ // 400/800电话无区号
+ if (Is400Phone(phone) || Is800Phone(phone))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(phone, @"[^\d]", "");
+
+ // 三位区号(0开头)
+ if (cleaned.Length >= 10 && cleaned.StartsWith("0"))
+ {
+ string code3 = cleaned.Substring(0, 3);
+ if (AreaCodeMap.ContainsKey(code3))
+ {
+ return code3;
+ }
+ }
+
+ // 四位区号(0开头)
+ if (cleaned.Length >= 11 && cleaned.StartsWith("0"))
+ {
+ string code4 = cleaned.Substring(0, 4);
+ if (AreaCodeMap.ContainsKey(code4))
+ {
+ return code4;
+ }
+ }
+
+ // 尝试提取前3-4位作为区号
+ if (cleaned.StartsWith("0"))
+ {
+ for (int len = Math.Min(4, cleaned.Length - 7); len >= 3; len--)
+ {
+ string code = cleaned.Substring(0, len);
+ if (AreaCodeMap.ContainsKey(code))
+ {
+ return code;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取城市名称
+ ///
+ /// 电话号码
+ /// 城市名称
+ public static string? GetCity(string? phone)
+ {
+ string? areaCode = GetAreaCode(phone);
+ if (areaCode == null)
+ {
+ return null;
+ }
+
+ return AreaCodeMap.TryGetValue(areaCode, out string? city) ? city : null;
+ }
+
+ ///
+ /// 获取本地号码(不含区号)
+ ///
+ /// 电话号码
+ /// 本地号码
+ public static string? GetLocalNumber(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ {
+ return null;
+ }
+
+ // 400/800电话
+ if (Is400Phone(phone) || Is800Phone(phone))
+ {
+ string local = Regex.Replace(phone, @"[^\d]", "");
+ return local.Length >= 10 ? local.Substring(3) : null;
+ }
+
+ string? areaCode = GetAreaCode(phone);
+ if (areaCode == null)
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(phone, @"[^\d]", "");
+ return cleaned.Substring(areaCode.Length);
+ }
+
+ ///
+ /// 获取电话类型
+ ///
+ /// 电话号码
+ /// 电话类型描述
+ public static string? GetPhoneType(string? phone)
+ {
+ if (Is400Phone(phone)) return "400企业热线";
+ if (Is800Phone(phone)) return "800免费电话";
+ if (IsLandline(phone)) return "固定电话";
+ return null;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化电话号码(去除非数字字符)
+ ///
+ /// 电话号码
+ /// 格式化后的号码
+ public static string? Normalize(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(phone, @"[^\d]", "");
+ return cleaned.Length >= 7 ? cleaned : null;
+ }
+
+ ///
+ /// 格式化为标准格式(区号-本地号码)
+ ///
+ /// 电话号码
+ /// 格式化后的号码
+ public static string? Format(string? phone)
+ {
+ string? normalized = Normalize(phone);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ // 400电话
+ if (normalized.StartsWith("400") && normalized.Length == 10)
+ {
+ return $"{normalized.Substring(0, 3)}-{normalized.Substring(3, 3)}-{normalized.Substring(6)}";
+ }
+
+ // 800电话
+ if (normalized.StartsWith("800") && normalized.Length == 10)
+ {
+ return $"{normalized.Substring(0, 3)}-{normalized.Substring(3, 3)}-{normalized.Substring(6)}";
+ }
+
+ // 带区号的固定电话
+ string? areaCode = GetAreaCode(normalized);
+ if (areaCode != null)
+ {
+ string local = normalized.Substring(areaCode.Length);
+ return $"{areaCode}-{local}";
+ }
+
+ return normalized;
+ }
+
+ ///
+ /// 电话号码脱敏:010-****1234
+ ///
+ /// 电话号码
+ /// 脱敏后的号码
+ public static string? Mask(string? phone)
+ {
+ if (!IsValid(phone))
+ {
+ return null;
+ }
+
+ string? areaCode = GetAreaCode(phone);
+ string? local = GetLocalNumber(phone);
+
+ if (areaCode != null && local != null && local.Length >= 4)
+ {
+ int visibleSuffix = 4;
+ int maskLen = local.Length - visibleSuffix;
+ return $"{areaCode}-{new string('*', maskLen)}{local.Substring(maskLen)}";
+ }
+
+ // 400/800电话
+ string? normalized = Normalize(phone);
+ if (normalized != null && normalized.Length == 10)
+ {
+ return $"{normalized.Substring(0, 3)}-****{normalized.Substring(6)}";
+ }
+
+ return null;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/PortUtil.cs b/EasyTool.Core/BusinessCategory/PortUtil.cs
new file mode 100644
index 0000000..b2adf9b
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/PortUtil.cs
@@ -0,0 +1,447 @@
+using System;
+using System.Collections.Generic;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 端口号工具类
+ ///
+ public static class PortUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 知名端口号范围(0-1023)
+ ///
+ public const int WellKnownPortMin = 0;
+
+ ///
+ /// 知名端口号范围上限
+ ///
+ public const int WellKnownPortMax = 1023;
+
+ ///
+ /// 注册端口号范围(1024-49151)
+ ///
+ public const int RegisteredPortMin = 1024;
+
+ ///
+ /// 注册端口号范围上限
+ ///
+ public const int RegisteredPortMax = 49151;
+
+ ///
+ /// 动态/私有端口号范围(49152-65535)
+ ///
+ public const int DynamicPortMin = 49152;
+
+ ///
+ /// 最大端口号
+ ///
+ public const int MaxPort = 65535;
+
+ ///
+ /// 常见端口与名称映射
+ ///
+ private static readonly Dictionary CommonPorts = new()
+ {
+ // 文件传输
+ { 20, new PortInfo("FTP Data", "文件传输协议数据端口", "FTP") },
+ { 21, new PortInfo("FTP", "文件传输协议控制端口", "FTP") },
+
+ // 远程连接
+ { 22, new PortInfo("SSH", "安全外壳协议", "SSH") },
+ { 23, new PortInfo("Telnet", "远程终端协议", "Telnet") },
+ { 3389, new PortInfo("RDP", "远程桌面协议", "RDP") },
+
+ // 邮件服务
+ { 25, new PortInfo("SMTP", "简单邮件传输协议", "SMTP") },
+ { 110, new PortInfo("POP3", "邮局协议第3版", "POP3") },
+ { 143, new PortInfo("IMAP", "互联网消息访问协议", "IMAP") },
+ { 465, new PortInfo("SMTPS", "SMTP安全协议", "SMTPS") },
+ { 587, new PortInfo("SMTP(TLS)", "SMTP TLS协议", "SMTP") },
+ { 993, new PortInfo("IMAPS", "IMAP安全协议", "IMAPS") },
+ { 995, new PortInfo("POP3S", "POP3安全协议", "POP3S") },
+
+ // Web服务
+ { 80, new PortInfo("HTTP", "超文本传输协议", "HTTP") },
+ { 443, new PortInfo("HTTPS", "HTTP安全协议", "HTTPS") },
+ { 8080, new PortInfo("HTTP-Proxy", "HTTP代理/备用端口", "HTTP") },
+ { 8443, new PortInfo("HTTPS-Alt", "HTTPS备用端口", "HTTPS") },
+
+ // 域名服务
+ { 53, new PortInfo("DNS", "域名系统", "DNS") },
+
+ // 数据库
+ { 1433, new PortInfo("MSSQL", "Microsoft SQL Server", "MSSQL") },
+ { 1521, new PortInfo("Oracle", "Oracle数据库", "Oracle") },
+ { 3306, new PortInfo("MySQL", "MySQL数据库", "MySQL") },
+ { 5432, new PortInfo("PostgreSQL", "PostgreSQL数据库", "PostgreSQL") },
+ { 6379, new PortInfo("Redis", "Redis缓存", "Redis") },
+ { 27017, new PortInfo("MongoDB", "MongoDB数据库", "MongoDB") },
+ { 9200, new PortInfo("Elasticsearch", "Elasticsearch搜索", "Elasticsearch") },
+
+ // 消息队列
+ { 5672, new PortInfo("RabbitMQ", "RabbitMQ消息队列", "RabbitMQ") },
+ { 9092, new PortInfo("Kafka", "Kafka消息队列", "Kafka") },
+ { 61616, new PortInfo("ActiveMQ", "ActiveMQ消息队列", "ActiveMQ") },
+
+ // 网络服务
+ { 67, new PortInfo("DHCP Server", "DHCP服务器", "DHCP") },
+ { 68, new PortInfo("DHCP Client", "DHCP客户端", "DHCP") },
+ { 69, new PortInfo("TFTP", "简单文件传输协议", "TFTP") },
+ { 123, new PortInfo("NTP", "网络时间协议", "NTP") },
+ { 161, new PortInfo("SNMP", "简单网络管理协议", "SNMP") },
+ { 162, new PortInfo("SNMP Trap", "SNMP陷阱", "SNMP") },
+ { 514, new PortInfo("Syslog", "系统日志", "Syslog") },
+
+ // VPN
+ { 500, new PortInfo("IKE", "Internet密钥交换", "VPN") },
+ { 1194, new PortInfo("OpenVPN", "OpenVPN", "VPN") },
+ { 1723, new PortInfo("PPTP", "点对点隧道协议", "VPN") },
+
+ // 其他常用
+ { 88, new PortInfo("Kerberos", "Kerberos认证", "Kerberos") },
+ { 389, new PortInfo("LDAP", "轻量级目录访问协议", "LDAP") },
+ { 636, new PortInfo("LDAPS", "LDAP安全协议", "LDAP") },
+ { 4444, new PortInfo("Kerberos-Admin", "Kerberos管理", "Kerberos") },
+
+ // 即时通讯
+ { 5222, new PortInfo("XMPP", "XMPP客户端连接", "XMPP") },
+ { 5269, new PortInfo("XMPP-Server", "XMPP服务器连接", "XMPP") },
+
+ // 游戏服务
+ { 25565, new PortInfo("Minecraft", "Minecraft服务器", "Minecraft") },
+ { 27015, new PortInfo("Steam", "Steam游戏服务", "Steam") },
+
+ // 文件共享
+ { 139, new PortInfo("NetBIOS-SSN", "NetBIOS会话服务", "NetBIOS") },
+ { 445, new PortInfo("SMB", "Server Message Block", "SMB") },
+ { 2049, new PortInfo("NFS", "网络文件系统", "NFS") },
+
+ // 代理服务
+ { 1080, new PortInfo("SOCKS", "SOCKS代理", "SOCKS") },
+ { 3128, new PortInfo("Squid", "Squid代理", "Squid") },
+
+ // Java相关
+ { 1099, new PortInfo("RMI", "Java RMI注册", "RMI") },
+ { 8009, new PortInfo("AJP", "Apache JServ协议", "AJP") },
+
+ // 监控
+ { 9090, new PortInfo("Prometheus", "Prometheus监控", "Prometheus") },
+ { 3000, new PortInfo("Grafana", "Grafana监控", "Grafana") },
+ { 8500, new PortInfo("Consul", "Consul服务发现", "Consul") }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证端口号是否有效
+ ///
+ /// 端口号
+ /// 是否有效
+ public static bool IsValid(int port)
+ {
+ return port >= WellKnownPortMin && port <= MaxPort;
+ }
+
+ ///
+ /// 验证端口号字符串是否有效
+ ///
+ /// 端口号字符串
+ /// 是否有效
+ public static bool IsValid(string? port)
+ {
+ if (string.IsNullOrWhiteSpace(port))
+ {
+ return false;
+ }
+
+ if (!int.TryParse(port, out int portNum))
+ {
+ return false;
+ }
+
+ return IsValid(portNum);
+ }
+
+ #endregion
+
+ #region 端口类型判断
+
+ ///
+ /// 判断是否为知名端口(0-1023)
+ ///
+ /// 端口号
+ /// 是否为知名端口
+ public static bool IsWellKnownPort(int port)
+ {
+ return port >= WellKnownPortMin && port <= WellKnownPortMax;
+ }
+
+ ///
+ /// 判断是否为注册端口(1024-49151)
+ ///
+ /// 端口号
+ /// 是否为注册端口
+ public static bool IsRegisteredPort(int port)
+ {
+ return port >= RegisteredPortMin && port <= RegisteredPortMax;
+ }
+
+ ///
+ /// 判断是否为动态/私有端口(49152-65535)
+ ///
+ /// 端口号
+ /// 是否为动态端口
+ public static bool IsDynamicPort(int port)
+ {
+ return port >= DynamicPortMin && port <= MaxPort;
+ }
+
+ ///
+ /// 获取端口类型
+ ///
+ /// 端口号
+ /// 端口类型
+ public static PortType GetPortType(int port)
+ {
+ if (IsWellKnownPort(port))
+ {
+ return PortType.WellKnown;
+ }
+ else if (IsRegisteredPort(port))
+ {
+ return PortType.Registered;
+ }
+ else if (IsDynamicPort(port))
+ {
+ return PortType.Dynamic;
+ }
+ else
+ {
+ return PortType.Invalid;
+ }
+ }
+
+ ///
+ /// 获取端口类型名称
+ ///
+ /// 端口号
+ /// 端口类型名称
+ public static string? GetPortTypeName(int port)
+ {
+ return GetPortType(port) switch
+ {
+ PortType.WellKnown => "知名端口",
+ PortType.Registered => "注册端口",
+ PortType.Dynamic => "动态/私有端口",
+ _ => null
+ };
+ }
+
+ #endregion
+
+ #region 端口信息
+
+ ///
+ /// 获取端口信息
+ ///
+ /// 端口号
+ /// 端口信息
+ public static PortInfo? GetPortInfo(int port)
+ {
+ if (!IsValid(port))
+ {
+ return null;
+ }
+
+ return CommonPorts.TryGetValue(port, out PortInfo? info) ? info : null;
+ }
+
+ ///
+ /// 获取端口名称
+ ///
+ /// 端口号
+ /// 端口名称
+ public static string? GetPortName(int port)
+ {
+ return GetPortInfo(port)?.Name;
+ }
+
+ ///
+ /// 获取端口描述
+ ///
+ /// 端口号
+ /// 端口描述
+ public static string? GetPortDescription(int port)
+ {
+ return GetPortInfo(port)?.Description;
+ }
+
+ ///
+ /// 获取端口所属服务类别
+ ///
+ /// 端口号
+ /// 服务类别
+ public static string? GetPortCategory(int port)
+ {
+ return GetPortInfo(port)?.Category;
+ }
+
+ ///
+ /// 判断是否为常见端口
+ ///
+ /// 端口号
+ /// 是否为常见端口
+ public static bool IsCommonPort(int port)
+ {
+ return CommonPorts.ContainsKey(port);
+ }
+
+ #endregion
+
+ #region 范围操作
+
+ ///
+ /// 获取指定范围内的所有端口
+ ///
+ /// 起始端口
+ /// 结束端口
+ /// 端口列表
+ public static int[] GetPortRange(int start, int end)
+ {
+ if (start < WellKnownPortMin || end > MaxPort || start > end)
+ {
+ return Array.Empty();
+ }
+
+ int[] ports = new int[end - start + 1];
+ for (int i = 0; i < ports.Length; i++)
+ {
+ ports[i] = start + i;
+ }
+ return ports;
+ }
+
+ ///
+ /// 获取所有知名端口
+ ///
+ /// 知名端口数组
+ public static int[] GetWellKnownPorts()
+ {
+ return GetPortRange(WellKnownPortMin, WellKnownPortMax);
+ }
+
+ ///
+ /// 获取所有动态端口范围
+ ///
+ /// 动态端口数组
+ public static int[] GetDynamicPorts()
+ {
+ return GetPortRange(DynamicPortMin, MaxPort);
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化端口号为字符串
+ ///
+ /// 端口号
+ /// 端口号字符串
+ public static string? Format(int port)
+ {
+ if (!IsValid(port))
+ {
+ return null;
+ }
+
+ return port.ToString();
+ }
+
+ ///
+ /// 格式化端口信息
+ ///
+ /// 端口号
+ /// 格式化的端口信息
+ public static string? FormatWithInfo(int port)
+ {
+ if (!IsValid(port))
+ {
+ return null;
+ }
+
+ PortInfo? info = GetPortInfo(port);
+ if (info != null)
+ {
+ return $"{port} ({info.Name})";
+ }
+
+ string? typeName = GetPortTypeName(port);
+ return $"{port} ({typeName})";
+ }
+
+ #endregion
+ }
+
+ ///
+ /// 端口类型枚举
+ ///
+ public enum PortType
+ {
+ ///
+ /// 无效端口
+ ///
+ Invalid = 0,
+
+ ///
+ /// 知名端口(0-1023)
+ ///
+ WellKnown = 1,
+
+ ///
+ /// 注册端口(1024-49151)
+ ///
+ Registered = 2,
+
+ ///
+ /// 动态/私有端口(49152-65535)
+ ///
+ Dynamic = 3
+ }
+
+ ///
+ /// 端口信息类
+ ///
+ public class PortInfo
+ {
+ ///
+ /// 端口名称
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 端口描述
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// 服务类别
+ ///
+ public string Category { get; set; }
+
+ ///
+ /// 构造函数
+ ///
+ public PortInfo(string name, string description, string category)
+ {
+ Name = name;
+ Description = description;
+ Category = category;
+ }
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/PostalCodeUtil.cs b/EasyTool.Core/BusinessCategory/PostalCodeUtil.cs
new file mode 100644
index 0000000..b4112de
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/PostalCodeUtil.cs
@@ -0,0 +1,437 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 邮政编码工具类
+ ///
+ public static class PostalCodeUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 中国邮政编码正则表达式(6位数字)
+ ///
+ private static readonly Regex PostalCodeRegex = new Regex(@"^\d{6}$", RegexOptions.Compiled);
+
+ ///
+ /// 省份编码前缀与名称映射(邮政编码前2位)
+ ///
+ private static readonly Dictionary ProvincePrefixMap = new Dictionary
+ {
+ { "10", "北京市" }, { "11", "北京市" }, { "12", "天津市" },
+ { "01", "上海市" }, { "02", "上海市" }, { "03", "上海市" }, { "20", "上海市" },
+ { "05", "河北省" }, { "06", "河北省" }, { "07", "河北省" },
+ { "03", "山西省" }, { "04", "山西省" }, { "03", "内蒙古自治区" }, { "01", "内蒙古自治区" }, { "02", "内蒙古自治区" },
+ { "11", "辽宁省" }, { "12", "辽宁省" },
+ { "13", "吉林省" }, { "10", "吉林省" },
+ { "15", "黑龙江省" }, { "16", "黑龙江省" },
+ { "21", "江苏省" }, { "22", "江苏省" },
+ { "31", "浙江省" }, { "32", "浙江省" },
+ { "23", "安徽省" }, { "24", "安徽省" },
+ { "35", "福建省" }, { "36", "福建省" },
+ { "33", "江西省" }, { "34", "江西省" },
+ { "25", "山东省" }, { "26", "山东省" }, { "27", "山东省" },
+ { "45", "河南省" }, { "46", "河南省" }, { "47", "河南省" },
+ { "41", "湖北省" }, { "42", "湖北省" }, { "43", "湖北省" }, { "44", "湖北省" },
+ { "41", "湖南省" }, { "42", "湖南省" }, { "43", "湖南省" },
+ { "51", "广东省" }, { "52", "广东省" }, { "53", "广东省" },
+ { "54", "广西壮族自治区" }, { "55", "广西壮族自治区" },
+ { "57", "海南省" }, { "58", "海南省" },
+ { "40", "重庆市" },
+ { "61", "四川省" }, { "62", "四川省" }, { "63", "四川省" }, { "64", "四川省" },
+ { "55", "贵州省" }, { "56", "贵州省" },
+ { "65", "云南省" }, { "66", "云南省" }, { "67", "云南省" },
+ { "85", "西藏自治区" }, { "86", "西藏自治区" },
+ { "71", "陕西省" }, { "72", "陕西省" }, { "73", "陕西省" },
+ { "73", "甘肃省" }, { "74", "甘肃省" },
+ { "81", "青海省" }, { "82", "青海省" }, { "83", "青海省" },
+ { "75", "宁夏回族自治区" },
+ { "83", "新疆维吾尔自治区" }, { "84", "新疆维吾尔自治区" }
+ };
+
+ ///
+ /// 城市邮政编码范围映射(部分主要城市)
+ ///
+ private static readonly Dictionary CityCodeRanges = new Dictionary
+ {
+ // 直辖市
+ { "北京", ("100000", "102999", "北京市") },
+ { "上海", ("200000", "202999", "上海市") },
+ { "天津", ("300000", "302999", "天津市") },
+ { "重庆", ("400000", "409999", "重庆市") },
+
+ // 省会城市
+ { "石家庄", ("050000", "052999", "石家庄市") },
+ { "太原", ("030000", "032999", "太原市") },
+ { "呼和浩特", ("010000", "012999", "呼和浩特市") },
+ { "沈阳", ("110000", "112999", "沈阳市") },
+ { "长春", ("130000", "132999", "长春市") },
+ { "哈尔滨", ("150000", "152999", "哈尔滨市") },
+ { "南京", ("210000", "212999", "南京市") },
+ { "杭州", ("310000", "312999", "杭州市") },
+ { "合肥", ("230000", "232999", "合肥市") },
+ { "福州", ("350000", "352999", "福州市") },
+ { "南昌", ("330000", "332999", "南昌市") },
+ { "济南", ("250000", "252999", "济南市") },
+ { "郑州", ("450000", "452999", "郑州市") },
+ { "武汉", ("430000", "432999", "武汉市") },
+ { "长沙", ("410000", "412999", "长沙市") },
+ { "广州", ("510000", "512999", "广州市") },
+ { "南宁", ("530000", "532999", "南宁市") },
+ { "海口", ("570000", "572999", "海口市") },
+ { "成都", ("610000", "612999", "成都市") },
+ { "贵阳", ("550000", "552999", "贵阳市") },
+ { "昆明", ("650000", "652999", "昆明市") },
+ { "拉萨", ("850000", "852999", "拉萨市") },
+ { "西安", ("710000", "712999", "西安市") },
+ { "兰州", ("730000", "732999", "兰州市") },
+ { "西宁", ("810000", "812999", "西宁市") },
+ { "银川", ("750000", "752999", "银川市") },
+ { "乌鲁木齐", ("830000", "832999", "乌鲁木齐市") },
+
+ // 重要城市
+ { "深圳", ("518000", "518999", "深圳市") },
+ { "珠海", ("519000", "519999", "珠海市") },
+ { "汕头", ("515000", "515999", "汕头市") },
+ { "佛山", ("528000", "528999", "佛山市") },
+ { "东莞", ("523000", "523999", "东莞市") },
+ { "中山", ("528400", "528499", "中山市") },
+ { "苏州", ("215000", "215999", "苏州市") },
+ { "无锡", ("214000", "214999", "无锡市") },
+ { "宁波", ("315000", "315999", "宁波市") },
+ { "温州", ("325000", "325999", "温州市") },
+ { "青岛", ("266000", "266999", "青岛市") },
+ { "大连", ("116000", "116999", "大连市") },
+ { "厦门", ("361000", "361999", "厦门市") }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证邮政编码格式是否有效
+ ///
+ /// 邮政编码
+ /// 是否有效
+ public static bool IsValid(string? postalCode)
+ {
+ if (string.IsNullOrWhiteSpace(postalCode))
+ {
+ return false;
+ }
+
+ return PostalCodeRegex.IsMatch(postalCode);
+ }
+
+ ///
+ /// 验证邮政编码是否有效且存在对应的省份
+ ///
+ /// 邮政编码
+ /// 是否为有效且存在的邮政编码
+ public static bool IsValidAndExists(string? postalCode)
+ {
+ if (!IsValid(postalCode))
+ {
+ return false;
+ }
+
+ return GetProvince(postalCode) != null;
+ }
+
+ #endregion
+
+ #region 信息查询
+
+ ///
+ /// 获取省份名称
+ ///
+ /// 邮政编码
+ /// 省份名称
+ public static string? GetProvince(string? postalCode)
+ {
+ if (!IsValid(postalCode))
+ {
+ return null;
+ }
+
+ string prefix = postalCode!.Substring(0, 2);
+
+ // 特殊处理直辖市
+ if (prefix == "10" || prefix == "11")
+ {
+ return "北京市";
+ }
+ if (prefix == "12")
+ {
+ return "天津市";
+ }
+ if (prefix == "20" || prefix == "01" || prefix == "02")
+ {
+ return "上海市";
+ }
+ if (prefix == "40")
+ {
+ return "重庆市";
+ }
+
+ // 根据前2位判断省份
+ return prefix switch
+ {
+ "05" or "06" or "07" => "河北省",
+ "03" or "04" => "山西省",
+ "01" or "02" => CheckInnerMongolia(postalCode) ? "内蒙古自治区" : null,
+ "11" or "12" => "辽宁省",
+ "13" => "吉林省",
+ "15" or "16" => "黑龙江省",
+ "21" or "22" => "江苏省",
+ "31" or "32" => "浙江省",
+ "23" or "24" => "安徽省",
+ "35" or "36" => "福建省",
+ "33" or "34" => "江西省",
+ "25" or "26" or "27" => "山东省",
+ "45" or "46" or "47" => "河南省",
+ "43" or "44" => "湖北省",
+ "41" or "42" => "湖南省",
+ "51" or "52" or "53" => "广东省",
+ "54" or "55" => "广西壮族自治区",
+ "57" or "58" => "海南省",
+ "61" or "62" or "63" or "64" => "四川省",
+ "55" or "56" => "贵州省",
+ "65" or "66" or "67" => "云南省",
+ "85" or "86" => "西藏自治区",
+ "71" or "72" or "73" => "陕西省",
+ "73" or "74" => "甘肃省",
+ "81" or "82" => "青海省",
+ "75" => "宁夏回族自治区",
+ "83" or "84" => "新疆维吾尔自治区",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取城市名称(部分城市支持)
+ ///
+ /// 邮政编码
+ /// 城市名称
+ public static string? GetCity(string? postalCode)
+ {
+ if (!IsValid(postalCode))
+ {
+ return null;
+ }
+
+ string code = postalCode!;
+
+ // 遍历城市编码范围
+ foreach (var kvp in CityCodeRanges)
+ {
+ if (string.Compare(code, kvp.Value.Min) >= 0 && string.Compare(code, kvp.Value.Max) <= 0)
+ {
+ return kvp.Value.City;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 根据城市名称查询邮政编码(返回主要邮编)
+ ///
+ /// 城市名称
+ /// 邮政编码,未找到返回null
+ public static string? GetPostalCodeByCity(string? cityName)
+ {
+ if (string.IsNullOrWhiteSpace(cityName))
+ {
+ return null;
+ }
+
+ // 处理常见城市名称变体
+ string normalizedCity = cityName.Replace("市", "").Trim();
+
+ foreach (var kvp in CityCodeRanges)
+ {
+ if (kvp.Key.Contains(normalizedCity) || normalizedCity.Contains(kvp.Key))
+ {
+ return kvp.Value.Min;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取邮政编码前缀(前2位)
+ ///
+ /// 邮政编码
+ /// 前缀
+ public static string? GetPrefix(string? postalCode)
+ {
+ if (!IsValid(postalCode))
+ {
+ return null;
+ }
+
+ return postalCode!.Substring(0, 2);
+ }
+
+ ///
+ /// 获取邮政编码后缀(后4位)
+ ///
+ /// 邮政编码
+ /// 后缀
+ public static string? GetSuffix(string? postalCode)
+ {
+ if (!IsValid(postalCode))
+ {
+ return null;
+ }
+
+ return postalCode!.Substring(2, 4);
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化邮政编码(去除非数字字符)
+ ///
+ /// 邮政编码
+ /// 格式化后的邮政编码
+ public static string? Normalize(string? postalCode)
+ {
+ if (string.IsNullOrWhiteSpace(postalCode))
+ {
+ return null;
+ }
+
+ // 去除所有非数字字符
+ string normalized = Regex.Replace(postalCode, @"\D", "");
+
+ if (normalized.Length != 6)
+ {
+ return null;
+ }
+
+ return normalized;
+ }
+
+ ///
+ /// 邮政编码脱敏:100***
+ ///
+ /// 邮政编码
+ /// 脱敏后的邮政编码
+ public static string? Mask(string? postalCode)
+ {
+ if (!IsValid(postalCode))
+ {
+ return null;
+ }
+
+ return postalCode!.Substring(0, 3) + "***";
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机邮政编码(仅供测试使用)
+ ///
+ /// 省份名称(可选,默认随机)
+ /// 6位邮政编码
+ public static string GenerateRandom(string? province = null)
+ {
+ if (!string.IsNullOrWhiteSpace(province))
+ {
+ // 根据省份生成
+ string prefix = GetProvincePrefix(province);
+ if (!string.IsNullOrEmpty(prefix))
+ {
+ return prefix + MathCategory.RandomUtil.RandomDigitString(4);
+ }
+ }
+
+ // 随机生成有效前缀
+ string[] validPrefixes = {
+ "10", "11", "12", "20", "30", "40",
+ "05", "06", "07", "03", "04", "01", "02",
+ "11", "12", "13", "15", "16",
+ "21", "22", "31", "32", "23", "24",
+ "35", "36", "33", "34", "25", "26", "27",
+ "45", "46", "47", "43", "44", "41", "42",
+ "51", "52", "53", "54", "55", "57", "58",
+ "40", "61", "62", "63", "64", "65", "66", "67",
+ "85", "86", "71", "72", "73", "74", "75", "81", "82", "83", "84"
+ };
+
+ string randomPrefix = MathCategory.RandomUtil.GetRandomElement(validPrefixes);
+ return randomPrefix + MathCategory.RandomUtil.RandomDigitString(4);
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 检查是否为内蒙古邮编
+ ///
+ private static bool CheckInnerMongolia(string postalCode)
+ {
+ // 内蒙古邮编范围:010000-029999
+ string prefix = postalCode.Substring(0, 2);
+ return prefix == "01" || prefix == "02";
+ }
+
+ ///
+ /// 根据省份名称获取邮编前缀
+ ///
+ private static string? GetProvincePrefix(string province)
+ {
+ string normalized = province.Replace("省", "").Replace("市", "").Replace("自治区", "").Trim();
+
+ return normalized switch
+ {
+ "北京" => "10",
+ "上海" => "20",
+ "天津" => "30",
+ "重庆" => "40",
+ "河北" => "05",
+ "山西" => "03",
+ "内蒙古" => "01",
+ "辽宁" => "11",
+ "吉林" => "13",
+ "黑龙江" => "15",
+ "江苏" => "21",
+ "浙江" => "31",
+ "安徽" => "23",
+ "福建" => "35",
+ "江西" => "33",
+ "山东" => "25",
+ "河南" => "45",
+ "湖北" => "43",
+ "湖南" => "41",
+ "广东" => "51",
+ "广西" => "54",
+ "海南" => "57",
+ "四川" => "61",
+ "贵州" => "55",
+ "云南" => "65",
+ "西藏" => "85",
+ "陕西" => "71",
+ "甘肃" => "73",
+ "青海" => "81",
+ "宁夏" => "75",
+ "新疆" => "83",
+ _ => null
+ };
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/ProvinceUtil.cs b/EasyTool.Core/BusinessCategory/ProvinceUtil.cs
new file mode 100644
index 0000000..acdfd75
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/ProvinceUtil.cs
@@ -0,0 +1,573 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 中国省份城市工具类
+ /// 提供省份、城市查询和验证功能
+ ///
+ public static class ProvinceUtil
+ {
+ ///
+ /// 省份数据
+ ///
+ private static readonly Dictionary Provinces = new()
+ {
+ { "110000", new ProvinceInfo { Code = "110000", Name = "北京市", ShortName = "北京", Cities = new List {
+ new CityInfo { Code = "110100", Name = "北京市" }
+ }}},
+ { "120000", new ProvinceInfo { Code = "120000", Name = "天津市", ShortName = "天津", Cities = new List {
+ new CityInfo { Code = "120100", Name = "天津市" }
+ }}},
+ { "130000", new ProvinceInfo { Code = "130000", Name = "河北省", ShortName = "河北", Cities = new List {
+ new CityInfo { Code = "130100", Name = "石家庄市" },
+ new CityInfo { Code = "130200", Name = "唐山市" },
+ new CityInfo { Code = "130300", Name = "秦皇岛市" },
+ new CityInfo { Code = "130400", Name = "邯郸市" },
+ new CityInfo { Code = "130500", Name = "邢台市" },
+ new CityInfo { Code = "130600", Name = "保定市" },
+ new CityInfo { Code = "130700", Name = "张家口市" },
+ new CityInfo { Code = "130800", Name = "承德市" },
+ new CityInfo { Code = "130900", Name = "沧州市" },
+ new CityInfo { Code = "131000", Name = "廊坊市" },
+ new CityInfo { Code = "131100", Name = "衡水市" }
+ }}},
+ { "140000", new ProvinceInfo { Code = "140000", Name = "山西省", ShortName = "山西", Cities = new List {
+ new CityInfo { Code = "140100", Name = "太原市" },
+ new CityInfo { Code = "140200", Name = "大同市" },
+ new CityInfo { Code = "140300", Name = "阳泉市" },
+ new CityInfo { Code = "140400", Name = "长治市" },
+ new CityInfo { Code = "140500", Name = "晋城市" },
+ new CityInfo { Code = "140600", Name = "朔州市" },
+ new CityInfo { Code = "140700", Name = "晋中市" },
+ new CityInfo { Code = "140800", Name = "运城市" },
+ new CityInfo { Code = "140900", Name = "忻州市" },
+ new CityInfo { Code = "141000", Name = "临汾市" },
+ new CityInfo { Code = "141100", Name = "吕梁市" }
+ }}},
+ { "150000", new ProvinceInfo { Code = "150000", Name = "内蒙古自治区", ShortName = "内蒙古", Cities = new List {
+ new CityInfo { Code = "150100", Name = "呼和浩特市" },
+ new CityInfo { Code = "150200", Name = "包头市" },
+ new CityInfo { Code = "150300", Name = "乌海市" },
+ new CityInfo { Code = "150400", Name = "赤峰市" },
+ new CityInfo { Code = "150500", Name = "通辽市" },
+ new CityInfo { Code = "150600", Name = "鄂尔多斯市" },
+ new CityInfo { Code = "150700", Name = "呼伦贝尔市" },
+ new CityInfo { Code = "150800", Name = "巴彦淖尔市" },
+ new CityInfo { Code = "150900", Name = "乌兰察布市" }
+ }}},
+ { "210000", new ProvinceInfo { Code = "210000", Name = "辽宁省", ShortName = "辽宁", Cities = new List {
+ new CityInfo { Code = "210100", Name = "沈阳市" },
+ new CityInfo { Code = "210200", Name = "大连市" },
+ new CityInfo { Code = "210300", Name = "鞍山市" },
+ new CityInfo { Code = "210400", Name = "抚顺市" },
+ new CityInfo { Code = "210500", Name = "本溪市" },
+ new CityInfo { Code = "210600", Name = "丹东市" },
+ new CityInfo { Code = "210700", Name = "锦州市" },
+ new CityInfo { Code = "210800", Name = "营口市" },
+ new CityInfo { Code = "210900", Name = "阜新市" },
+ new CityInfo { Code = "211000", Name = "辽阳市" },
+ new CityInfo { Code = "211100", Name = "盘锦市" },
+ new CityInfo { Code = "211200", Name = "铁岭市" },
+ new CityInfo { Code = "211300", Name = "朝阳市" },
+ new CityInfo { Code = "211400", Name = "葫芦岛市" }
+ }}},
+ { "220000", new ProvinceInfo { Code = "220000", Name = "吉林省", ShortName = "吉林", Cities = new List {
+ new CityInfo { Code = "220100", Name = "长春市" },
+ new CityInfo { Code = "220200", Name = "吉林市" },
+ new CityInfo { Code = "220300", Name = "四平市" },
+ new CityInfo { Code = "220400", Name = "辽源市" },
+ new CityInfo { Code = "220500", Name = "通化市" },
+ new CityInfo { Code = "220600", Name = "白山市" },
+ new CityInfo { Code = "220700", Name = "松原市" },
+ new CityInfo { Code = "220800", Name = "白城市" }
+ }}},
+ { "230000", new ProvinceInfo { Code = "230000", Name = "黑龙江省", ShortName = "黑龙江", Cities = new List {
+ new CityInfo { Code = "230100", Name = "哈尔滨市" },
+ new CityInfo { Code = "230200", Name = "齐齐哈尔市" },
+ new CityInfo { Code = "230300", Name = "鸡西市" },
+ new CityInfo { Code = "230400", Name = "鹤岗市" },
+ new CityInfo { Code = "230500", Name = "双鸭山市" },
+ new CityInfo { Code = "230600", Name = "大庆市" },
+ new CityInfo { Code = "230700", Name = "伊春市" },
+ new CityInfo { Code = "230800", Name = "佳木斯市" },
+ new CityInfo { Code = "230900", Name = "七台河市" },
+ new CityInfo { Code = "231000", Name = "牡丹江市" },
+ new CityInfo { Code = "231100", Name = "黑河市" },
+ new CityInfo { Code = "231200", Name = "绥化市" }
+ }}},
+ { "310000", new ProvinceInfo { Code = "310000", Name = "上海市", ShortName = "上海", Cities = new List {
+ new CityInfo { Code = "310100", Name = "上海市" }
+ }}},
+ { "320000", new ProvinceInfo { Code = "320000", Name = "江苏省", ShortName = "江苏", Cities = new List {
+ new CityInfo { Code = "320100", Name = "南京市" },
+ new CityInfo { Code = "320200", Name = "无锡市" },
+ new CityInfo { Code = "320300", Name = "徐州市" },
+ new CityInfo { Code = "320400", Name = "常州市" },
+ new CityInfo { Code = "320500", Name = "苏州市" },
+ new CityInfo { Code = "320600", Name = "南通市" },
+ new CityInfo { Code = "320700", Name = "连云港市" },
+ new CityInfo { Code = "320800", Name = "淮安市" },
+ new CityInfo { Code = "320900", Name = "盐城市" },
+ new CityInfo { Code = "321000", Name = "扬州市" },
+ new CityInfo { Code = "321100", Name = "镇江市" },
+ new CityInfo { Code = "321200", Name = "泰州市" },
+ new CityInfo { Code = "321300", Name = "宿迁市" }
+ }}},
+ { "330000", new ProvinceInfo { Code = "330000", Name = "浙江省", ShortName = "浙江", Cities = new List {
+ new CityInfo { Code = "330100", Name = "杭州市" },
+ new CityInfo { Code = "330200", Name = "宁波市" },
+ new CityInfo { Code = "330300", Name = "温州市" },
+ new CityInfo { Code = "330400", Name = "嘉兴市" },
+ new CityInfo { Code = "330500", Name = "湖州市" },
+ new CityInfo { Code = "330600", Name = "绍兴市" },
+ new CityInfo { Code = "330700", Name = "金华市" },
+ new CityInfo { Code = "330800", Name = "衢州市" },
+ new CityInfo { Code = "330900", Name = "舟山市" },
+ new CityInfo { Code = "331000", Name = "台州市" },
+ new CityInfo { Code = "331100", Name = "丽水市" }
+ }}},
+ { "340000", new ProvinceInfo { Code = "340000", Name = "安徽省", ShortName = "安徽", Cities = new List {
+ new CityInfo { Code = "340100", Name = "合肥市" },
+ new CityInfo { Code = "340200", Name = "芜湖市" },
+ new CityInfo { Code = "340300", Name = "蚌埠市" },
+ new CityInfo { Code = "340400", Name = "淮南市" },
+ new CityInfo { Code = "340500", Name = "马鞍山市" },
+ new CityInfo { Code = "340600", Name = "淮北市" },
+ new CityInfo { Code = "340700", Name = "铜陵市" },
+ new CityInfo { Code = "340800", Name = "安庆市" },
+ new CityInfo { Code = "341000", Name = "黄山市" },
+ new CityInfo { Code = "341100", Name = "滁州市" },
+ new CityInfo { Code = "341200", Name = "阜阳市" },
+ new CityInfo { Code = "341300", Name = "宿州市" },
+ new CityInfo { Code = "341500", Name = "六安市" },
+ new CityInfo { Code = "341600", Name = "亳州市" },
+ new CityInfo { Code = "341700", Name = "池州市" },
+ new CityInfo { Code = "341800", Name = "宣城市" }
+ }}},
+ { "350000", new ProvinceInfo { Code = "350000", Name = "福建省", ShortName = "福建", Cities = new List {
+ new CityInfo { Code = "350100", Name = "福州市" },
+ new CityInfo { Code = "350200", Name = "厦门市" },
+ new CityInfo { Code = "350300", Name = "莆田市" },
+ new CityInfo { Code = "350400", Name = "三明市" },
+ new CityInfo { Code = "350500", Name = "泉州市" },
+ new CityInfo { Code = "350600", Name = "漳州市" },
+ new CityInfo { Code = "350700", Name = "南平市" },
+ new CityInfo { Code = "350800", Name = "龙岩市" },
+ new CityInfo { Code = "350900", Name = "宁德市" }
+ }}},
+ { "360000", new ProvinceInfo { Code = "360000", Name = "江西省", ShortName = "江西", Cities = new List {
+ new CityInfo { Code = "360100", Name = "南昌市" },
+ new CityInfo { Code = "360200", Name = "景德镇市" },
+ new CityInfo { Code = "360300", Name = "萍乡市" },
+ new CityInfo { Code = "360400", Name = "九江市" },
+ new CityInfo { Code = "360500", Name = "新余市" },
+ new CityInfo { Code = "360600", Name = "鹰潭市" },
+ new CityInfo { Code = "360700", Name = "赣州市" },
+ new CityInfo { Code = "360800", Name = "吉安市" },
+ new CityInfo { Code = "360900", Name = "宜春市" },
+ new CityInfo { Code = "361000", Name = "抚州市" },
+ new CityInfo { Code = "361100", Name = "上饶市" }
+ }}},
+ { "370000", new ProvinceInfo { Code = "370000", Name = "山东省", ShortName = "山东", Cities = new List {
+ new CityInfo { Code = "370100", Name = "济南市" },
+ new CityInfo { Code = "370200", Name = "青岛市" },
+ new CityInfo { Code = "370300", Name = "淄博市" },
+ new CityInfo { Code = "370400", Name = "枣庄市" },
+ new CityInfo { Code = "370500", Name = "东营市" },
+ new CityInfo { Code = "370600", Name = "烟台市" },
+ new CityInfo { Code = "370700", Name = "潍坊市" },
+ new CityInfo { Code = "370800", Name = "济宁市" },
+ new CityInfo { Code = "370900", Name = "泰安市" },
+ new CityInfo { Code = "371000", Name = "威海市" },
+ new CityInfo { Code = "371100", Name = "日照市" },
+ new CityInfo { Code = "371300", Name = "临沂市" },
+ new CityInfo { Code = "371400", Name = "德州市" },
+ new CityInfo { Code = "371500", Name = "聊城市" },
+ new CityInfo { Code = "371600", Name = "滨州市" },
+ new CityInfo { Code = "371700", Name = "菏泽市" }
+ }}},
+ { "410000", new ProvinceInfo { Code = "410000", Name = "河南省", ShortName = "河南", Cities = new List {
+ new CityInfo { Code = "410100", Name = "郑州市" },
+ new CityInfo { Code = "410200", Name = "开封市" },
+ new CityInfo { Code = "410300", Name = "洛阳市" },
+ new CityInfo { Code = "410400", Name = "平顶山市" },
+ new CityInfo { Code = "410500", Name = "安阳市" },
+ new CityInfo { Code = "410600", Name = "鹤壁市" },
+ new CityInfo { Code = "410700", Name = "新乡市" },
+ new CityInfo { Code = "410800", Name = "焦作市" },
+ new CityInfo { Code = "410900", Name = "濮阳市" },
+ new CityInfo { Code = "411000", Name = "许昌市" },
+ new CityInfo { Code = "411100", Name = "漯河市" },
+ new CityInfo { Code = "411200", Name = "三门峡市" },
+ new CityInfo { Code = "411300", Name = "南阳市" },
+ new CityInfo { Code = "411400", Name = "商丘市" },
+ new CityInfo { Code = "411500", Name = "信阳市" },
+ new CityInfo { Code = "411600", Name = "周口市" },
+ new CityInfo { Code = "411700", Name = "驻马店市" }
+ }}},
+ { "420000", new ProvinceInfo { Code = "420000", Name = "湖北省", ShortName = "湖北", Cities = new List {
+ new CityInfo { Code = "420100", Name = "武汉市" },
+ new CityInfo { Code = "420200", Name = "黄石市" },
+ new CityInfo { Code = "420300", Name = "十堰市" },
+ new CityInfo { Code = "420500", Name = "宜昌市" },
+ new CityInfo { Code = "420600", Name = "襄阳市" },
+ new CityInfo { Code = "420700", Name = "鄂州市" },
+ new CityInfo { Code = "420800", Name = "荆门市" },
+ new CityInfo { Code = "420900", Name = "孝感市" },
+ new CityInfo { Code = "421000", Name = "荆州市" },
+ new CityInfo { Code = "421100", Name = "黄冈市" },
+ new CityInfo { Code = "421200", Name = "咸宁市" },
+ new CityInfo { Code = "421300", Name = "随州市" }
+ }}},
+ { "430000", new ProvinceInfo { Code = "430000", Name = "湖南省", ShortName = "湖南", Cities = new List {
+ new CityInfo { Code = "430100", Name = "长沙市" },
+ new CityInfo { Code = "430200", Name = "株洲市" },
+ new CityInfo { Code = "430300", Name = "湘潭市" },
+ new CityInfo { Code = "430400", Name = "衡阳市" },
+ new CityInfo { Code = "430500", Name = "邵阳市" },
+ new CityInfo { Code = "430600", Name = "岳阳市" },
+ new CityInfo { Code = "430700", Name = "常德市" },
+ new CityInfo { Code = "430800", Name = "张家界市" },
+ new CityInfo { Code = "430900", Name = "益阳市" },
+ new CityInfo { Code = "431000", Name = "郴州市" },
+ new CityInfo { Code = "431100", Name = "永州市" },
+ new CityInfo { Code = "431200", Name = "怀化市" },
+ new CityInfo { Code = "431300", Name = "娄底市" }
+ }}},
+ { "440000", new ProvinceInfo { Code = "440000", Name = "广东省", ShortName = "广东", Cities = new List {
+ new CityInfo { Code = "440100", Name = "广州市" },
+ new CityInfo { Code = "440200", Name = "韶关市" },
+ new CityInfo { Code = "440300", Name = "深圳市" },
+ new CityInfo { Code = "440400", Name = "珠海市" },
+ new CityInfo { Code = "440500", Name = "汕头市" },
+ new CityInfo { Code = "440600", Name = "佛山市" },
+ new CityInfo { Code = "440700", Name = "江门市" },
+ new CityInfo { Code = "440800", Name = "湛江市" },
+ new CityInfo { Code = "440900", Name = "茂名市" },
+ new CityInfo { Code = "441200", Name = "肇庆市" },
+ new CityInfo { Code = "441300", Name = "惠州市" },
+ new CityInfo { Code = "441400", Name = "梅州市" },
+ new CityInfo { Code = "441500", Name = "汕尾市" },
+ new CityInfo { Code = "441600", Name = "河源市" },
+ new CityInfo { Code = "441700", Name = "阳江市" },
+ new CityInfo { Code = "441800", Name = "清远市" },
+ new CityInfo { Code = "441900", Name = "东莞市" },
+ new CityInfo { Code = "442000", Name = "中山市" },
+ new CityInfo { Code = "445100", Name = "潮州市" },
+ new CityInfo { Code = "445200", Name = "揭阳市" },
+ new CityInfo { Code = "445300", Name = "云浮市" }
+ }}},
+ { "450000", new ProvinceInfo { Code = "450000", Name = "广西壮族自治区", ShortName = "广西", Cities = new List {
+ new CityInfo { Code = "450100", Name = "南宁市" },
+ new CityInfo { Code = "450200", Name = "柳州市" },
+ new CityInfo { Code = "450300", Name = "桂林市" },
+ new CityInfo { Code = "450400", Name = "梧州市" },
+ new CityInfo { Code = "450500", Name = "北海市" },
+ new CityInfo { Code = "450600", Name = "防城港市" },
+ new CityInfo { Code = "450700", Name = "钦州市" },
+ new CityInfo { Code = "450800", Name = "贵港市" },
+ new CityInfo { Code = "450900", Name = "玉林市" },
+ new CityInfo { Code = "451000", Name = "百色市" },
+ new CityInfo { Code = "451100", Name = "贺州市" },
+ new CityInfo { Code = "451200", Name = "河池市" },
+ new CityInfo { Code = "451300", Name = "来宾市" },
+ new CityInfo { Code = "451400", Name = "崇左市" }
+ }}},
+ { "460000", new ProvinceInfo { Code = "460000", Name = "海南省", ShortName = "海南", Cities = new List {
+ new CityInfo { Code = "460100", Name = "海口市" },
+ new CityInfo { Code = "460200", Name = "三亚市" },
+ new CityInfo { Code = "460300", Name = "三沙市" },
+ new CityInfo { Code = "460400", Name = "儋州市" }
+ }}},
+ { "500000", new ProvinceInfo { Code = "500000", Name = "重庆市", ShortName = "重庆", Cities = new List {
+ new CityInfo { Code = "500100", Name = "重庆市" }
+ }}},
+ { "510000", new ProvinceInfo { Code = "510000", Name = "四川省", ShortName = "四川", Cities = new List {
+ new CityInfo { Code = "510100", Name = "成都市" },
+ new CityInfo { Code = "510300", Name = "自贡市" },
+ new CityInfo { Code = "510400", Name = "攀枝花市" },
+ new CityInfo { Code = "510500", Name = "泸州市" },
+ new CityInfo { Code = "510600", Name = "德阳市" },
+ new CityInfo { Code = "510700", Name = "绵阳市" },
+ new CityInfo { Code = "510800", Name = "广元市" },
+ new CityInfo { Code = "510900", Name = "遂宁市" },
+ new CityInfo { Code = "511000", Name = "内江市" },
+ new CityInfo { Code = "511100", Name = "乐山市" },
+ new CityInfo { Code = "511300", Name = "南充市" },
+ new CityInfo { Code = "511400", Name = "眉山市" },
+ new CityInfo { Code = "511500", Name = "宜宾市" },
+ new CityInfo { Code = "511600", Name = "广安市" },
+ new CityInfo { Code = "511700", Name = "达州市" },
+ new CityInfo { Code = "511800", Name = "雅安市" },
+ new CityInfo { Code = "511900", Name = "巴中市" },
+ new CityInfo { Code = "512000", Name = "资阳市" }
+ }}},
+ { "520000", new ProvinceInfo { Code = "520000", Name = "贵州省", ShortName = "贵州", Cities = new List {
+ new CityInfo { Code = "520100", Name = "贵阳市" },
+ new CityInfo { Code = "520200", Name = "六盘水市" },
+ new CityInfo { Code = "520300", Name = "遵义市" },
+ new CityInfo { Code = "520400", Name = "安顺市" },
+ new CityInfo { Code = "520500", Name = "毕节市" },
+ new CityInfo { Code = "520600", Name = "铜仁市" }
+ }}},
+ { "530000", new ProvinceInfo { Code = "530000", Name = "云南省", ShortName = "云南", Cities = new List {
+ new CityInfo { Code = "530100", Name = "昆明市" },
+ new CityInfo { Code = "530300", Name = "曲靖市" },
+ new CityInfo { Code = "530400", Name = "玉溪市" },
+ new CityInfo { Code = "530500", Name = "保山市" },
+ new CityInfo { Code = "530600", Name = "昭通市" },
+ new CityInfo { Code = "530700", Name = "丽江市" },
+ new CityInfo { Code = "530800", Name = "普洱市" },
+ new CityInfo { Code = "530900", Name = "临沧市" }
+ }}},
+ { "540000", new ProvinceInfo { Code = "540000", Name = "西藏自治区", ShortName = "西藏", Cities = new List {
+ new CityInfo { Code = "540100", Name = "拉萨市" },
+ new CityInfo { Code = "540200", Name = "日喀则市" },
+ new CityInfo { Code = "540300", Name = "昌都市" },
+ new CityInfo { Code = "540400", Name = "林芝市" },
+ new CityInfo { Code = "540500", Name = "山南市" },
+ new CityInfo { Code = "540600", Name = "那曲市" }
+ }}},
+ { "610000", new ProvinceInfo { Code = "610000", Name = "陕西省", ShortName = "陕西", Cities = new List {
+ new CityInfo { Code = "610100", Name = "西安市" },
+ new CityInfo { Code = "610200", Name = "铜川市" },
+ new CityInfo { Code = "610300", Name = "宝鸡市" },
+ new CityInfo { Code = "610400", Name = "咸阳市" },
+ new CityInfo { Code = "610500", Name = "渭南市" },
+ new CityInfo { Code = "610600", Name = "延安市" },
+ new CityInfo { Code = "610700", Name = "汉中市" },
+ new CityInfo { Code = "610800", Name = "榆林市" },
+ new CityInfo { Code = "610900", Name = "安康市" },
+ new CityInfo { Code = "611000", Name = "商洛市" }
+ }}},
+ { "620000", new ProvinceInfo { Code = "620000", Name = "甘肃省", ShortName = "甘肃", Cities = new List {
+ new CityInfo { Code = "620100", Name = "兰州市" },
+ new CityInfo { Code = "620200", Name = "嘉峪关市" },
+ new CityInfo { Code = "620300", Name = "金昌市" },
+ new CityInfo { Code = "620400", Name = "白银市" },
+ new CityInfo { Code = "620500", Name = "天水市" },
+ new CityInfo { Code = "620600", Name = "武威市" },
+ new CityInfo { Code = "620700", Name = "张掖市" },
+ new CityInfo { Code = "620800", Name = "平凉市" },
+ new CityInfo { Code = "620900", Name = "酒泉市" },
+ new CityInfo { Code = "621000", Name = "庆阳市" },
+ new CityInfo { Code = "621100", Name = "定西市" },
+ new CityInfo { Code = "621200", Name = "陇南市" }
+ }}},
+ { "630000", new ProvinceInfo { Code = "630000", Name = "青海省", ShortName = "青海", Cities = new List {
+ new CityInfo { Code = "630100", Name = "西宁市" },
+ new CityInfo { Code = "630200", Name = "海东市" }
+ }}},
+ { "640000", new ProvinceInfo { Code = "640000", Name = "宁夏回族自治区", ShortName = "宁夏", Cities = new List {
+ new CityInfo { Code = "640100", Name = "银川市" },
+ new CityInfo { Code = "640200", Name = "石嘴山市" },
+ new CityInfo { Code = "640300", Name = "吴忠市" },
+ new CityInfo { Code = "640400", Name = "固原市" },
+ new CityInfo { Code = "640500", Name = "中卫市" }
+ }}},
+ { "650000", new ProvinceInfo { Code = "650000", Name = "新疆维吾尔自治区", ShortName = "新疆", Cities = new List {
+ new CityInfo { Code = "650100", Name = "乌鲁木齐市" },
+ new CityInfo { Code = "650200", Name = "克拉玛依市" }
+ }}},
+ { "710000", new ProvinceInfo { Code = "710000", Name = "台湾省", ShortName = "台湾", Cities = new List() }},
+ { "810000", new ProvinceInfo { Code = "810000", Name = "香港特别行政区", ShortName = "香港", Cities = new List() }},
+ { "820000", new ProvinceInfo { Code = "820000", Name = "澳门特别行政区", ShortName = "澳门", Cities = new List() }}
+ };
+
+ ///
+ /// 根据省份代码获取省份信息
+ ///
+ public static ProvinceInfo? GetProvinceByCode(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code) || code.Length < 2)
+ return null;
+
+ var provinceCode = code.Substring(0, 2) + "0000";
+ return Provinces.TryGetValue(provinceCode, out var province) ? province : null;
+ }
+
+ ///
+ /// 根据省份名称获取省份信息
+ ///
+ public static ProvinceInfo? GetProvinceByName(string? name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return null;
+
+ foreach (var province in Provinces.Values)
+ {
+ if (province.Name == name || province.ShortName == name ||
+ province.Name.Contains(name) || name.Contains(province.ShortName))
+ {
+ return province;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 根据城市代码获取城市信息
+ ///
+ public static CityInfo? GetCityByCode(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code) || code.Length < 4)
+ return null;
+
+ var provinceCode = code.Substring(0, 2) + "0000";
+ if (!Provinces.TryGetValue(provinceCode, out var province))
+ return null;
+
+ foreach (var city in province.Cities)
+ {
+ if (city.Code == code)
+ return city;
+ }
+
+ return null;
+ }
+
+ ///
+ /// 根据城市名称获取城市信息
+ ///
+ public static CityInfo? GetCityByName(string? name, string? provinceName = null)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return null;
+
+ foreach (var province in Provinces.Values)
+ {
+ if (!string.IsNullOrEmpty(provinceName) &&
+ province.Name != provinceName &&
+ province.ShortName != provinceName)
+ continue;
+
+ foreach (var city in province.Cities)
+ {
+ if (city.Name == name || city.Name.Contains(name) || name.Contains(city.Name.Replace("市", "")))
+ {
+ return city;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取所有省份
+ ///
+ public static IEnumerable GetAllProvinces()
+ {
+ return Provinces.Values;
+ }
+
+ ///
+ /// 获取省份下的所有城市
+ ///
+ public static IEnumerable GetCitiesByProvinceCode(string? provinceCode)
+ {
+ var province = GetProvinceByCode(provinceCode);
+ return province?.Cities ?? Enumerable.Empty();
+ }
+
+ ///
+ /// 验证行政区划代码是否有效
+ ///
+ public static bool IsValidCode(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ return false;
+
+ code = code.Trim();
+ if (code.Length != 6)
+ return false;
+
+ foreach (var c in code)
+ {
+ if (!char.IsDigit(c))
+ return false;
+ }
+
+ var provinceCode = code.Substring(0, 2) + "0000";
+ return Provinces.ContainsKey(provinceCode);
+ }
+
+ ///
+ /// 根据身份证号前6位获取籍贯
+ ///
+ public static string? GetNativePlace(string? idCardPrefix)
+ {
+ if (string.IsNullOrWhiteSpace(idCardPrefix) || idCardPrefix.Length < 6)
+ return null;
+
+ var provinceCode = idCardPrefix.Substring(0, 2) + "0000";
+ if (!Provinces.TryGetValue(provinceCode, out var province))
+ return null;
+
+ var cityCode = idCardPrefix.Substring(0, 4) + "00";
+ foreach (var city in province.Cities)
+ {
+ if (city.Code == cityCode)
+ {
+ return $"{province.Name}{city.Name}";
+ }
+ }
+
+ return province.Name;
+ }
+ }
+
+ #region 数据类
+
+ ///
+ /// 省份信息
+ ///
+ public class ProvinceInfo
+ {
+ ///
+ /// 省份代码(6位)
+ ///
+ public string Code { get; set; } = string.Empty;
+
+ ///
+ /// 省份名称
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 简称
+ ///
+ public string ShortName { get; set; } = string.Empty;
+
+ ///
+ /// 城市列表
+ ///
+ public List Cities { get; set; } = new();
+
+ public override string ToString() => Name;
+ }
+
+ ///
+ /// 城市信息
+ ///
+ public class CityInfo
+ {
+ ///
+ /// 城市代码(6位)
+ ///
+ public string Code { get; set; } = string.Empty;
+
+ ///
+ /// 城市名称
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ public override string ToString() => Name;
+ }
+
+ #endregion
+}
diff --git a/EasyTool.Core/BusinessCategory/QQUtil.cs b/EasyTool.Core/BusinessCategory/QQUtil.cs
new file mode 100644
index 0000000..1357b92
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/QQUtil.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// QQ号工具类
+ ///
+ public static class QQUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// QQ号正则表达式(5-11位数字,不以0开头)
+ ///
+ private static readonly Regex QQRegex = new(
+ @"^[1-9]\d{4,10}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// QQ邮箱正则表达式
+ ///
+ private static readonly Regex QQEmailRegex = new(
+ @"^[1-9]\d{4,10}@qq\.com$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证QQ号是否有效
+ ///
+ /// QQ号
+ /// 是否有效
+ public static bool IsValid(string? qq)
+ {
+ if (string.IsNullOrWhiteSpace(qq))
+ {
+ return false;
+ }
+
+ return QQRegex.IsMatch(qq);
+ }
+
+ ///
+ /// 验证QQ邮箱是否有效
+ ///
+ /// QQ邮箱
+ /// 是否有效
+ public static bool IsValidQQEmail(string? email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ return false;
+ }
+
+ return QQEmailRegex.IsMatch(email);
+ }
+
+ ///
+ /// 验证QQ号格式(仅检查格式,不验证是否存在)
+ ///
+ /// QQ号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? qq)
+ {
+ return IsValid(qq);
+ }
+
+ #endregion
+
+ #region 转换方法
+
+ ///
+ /// 从QQ邮箱提取QQ号
+ ///
+ /// QQ邮箱
+ /// QQ号,提取失败返回null
+ public static string? ExtractFromEmail(string? email)
+ {
+ if (!IsValidQQEmail(email))
+ {
+ return null;
+ }
+
+ int atIndex = email!.IndexOf('@');
+ return email.Substring(0, atIndex);
+ }
+
+ ///
+ /// 将QQ号转换为QQ邮箱
+ ///
+ /// QQ号
+ /// QQ邮箱
+ public static string? ToEmail(string? qq)
+ {
+ if (!IsValid(qq))
+ {
+ return null;
+ }
+
+ return qq + "@qq.com";
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化QQ号(去除非数字字符)
+ ///
+ /// QQ号
+ /// 格式化后的QQ号
+ public static string? Normalize(string? qq)
+ {
+ if (string.IsNullOrWhiteSpace(qq))
+ {
+ return null;
+ }
+
+ string cleaned = Regex.Replace(qq, @"\D", "");
+ return IsValid(cleaned) ? cleaned : null;
+ }
+
+ ///
+ /// QQ号脱敏:123****890
+ ///
+ /// QQ号
+ /// 脱敏后的QQ号
+ public static string? Mask(string? qq)
+ {
+ if (!IsValid(qq))
+ {
+ return null;
+ }
+
+ string code = qq!;
+ if (code.Length <= 4)
+ {
+ return code[0] + new string('*', code.Length - 1);
+ }
+
+ // 保留前3位和后3位
+ int prefixLen = 3;
+ int suffixLen = 3;
+ int maskLen = code.Length - prefixLen - suffixLen;
+
+ return code.Substring(0, prefixLen) + new string('*', maskLen) + code.Substring(code.Length - suffixLen);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机QQ号(仅供测试使用)
+ ///
+ /// 随机QQ号
+ public static string GenerateRandom()
+ {
+ // QQ号长度5-11位
+ int length = MathCategory.RandomUtil.RandomInt(5, 12);
+
+ // 第一位不能为0
+ string result = MathCategory.RandomUtil.RandomInt(1, 10).ToString();
+
+ // 剩余位数
+ for (int i = 1; i < length; i++)
+ {
+ result += MathCategory.RandomUtil.RandomInt(0, 10).ToString();
+ }
+
+ return result;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/SocialSecurityUtil.cs b/EasyTool.Core/BusinessCategory/SocialSecurityUtil.cs
new file mode 100644
index 0000000..c033fbd
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/SocialSecurityUtil.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 社保号工具类
+ ///
+ public static class SocialSecurityUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 社保号正则表达式(18位,与身份证号格式相同)
+ ///
+ private static readonly Regex SSN18Regex = new(
+ @"^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 社保号正则表达式(部分省市为15位或16位)
+ ///
+ private static readonly Regex SSN15Regex = new(@"^\d{15,16}$", RegexOptions.Compiled);
+
+ ///
+ /// 社会保障卡号正则(带字母)
+ ///
+ private static readonly Regex SSNCardRegex = new(
+ @"^[A-Za-z]\d{17}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 校验码权重
+ ///
+ private static readonly int[] Weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
+
+ ///
+ /// 校验码对照表
+ ///
+ private static readonly char[] CheckCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };
+
+ ///
+ /// 省份社保号规则(简化版)
+ ///
+ private static readonly Dictionary ProvinceLengthMap = new()
+ {
+ { "北京", 18 }, { "上海", 18 }, { "天津", 18 }, { "重庆", 18 },
+ { "广东", 18 }, { "浙江", 18 }, { "江苏", 18 }, { "山东", 18 },
+ { "四川", 18 }, { "湖北", 18 }, { "河南", 18 }, { "河北", 18 },
+ { "福建", 18 }, { "安徽", 18 }, { "辽宁", 18 }, { "陕西", 18 },
+ { "湖南", 18 }, { "江西", 18 }, { "云南", 18 }, { "贵州", 18 },
+ { "甘肃", 18 }, { "青海", 18 }, { "宁夏", 18 }, { "新疆", 18 },
+ { "西藏", 18 }, { "内蒙古", 18 }, { "广西", 18 }, { "黑龙江", 18 },
+ { "吉林", 18 }, { "山西", 18 }, { "海南", 18 }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证社保号是否有效
+ ///
+ /// 社保号
+ /// 是否有效
+ public static bool IsValid(string? ssn)
+ {
+ if (string.IsNullOrWhiteSpace(ssn))
+ {
+ return false;
+ }
+
+ string cleaned = ssn.Trim().ToUpper();
+
+ // 18位(身份证号格式)
+ if (cleaned.Length == 18 && SSN18Regex.IsMatch(cleaned))
+ {
+ return ValidateCheckDigit(cleaned);
+ }
+
+ // 15-16位纯数字
+ if (SSN15Regex.IsMatch(cleaned))
+ {
+ return true;
+ }
+
+ // 带字母的卡号
+ if (SSNCardRegex.IsMatch(cleaned))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// 验证是否为18位社保号(身份证号格式)
+ ///
+ /// 社保号
+ /// 是否为18位
+ public static bool Is18Digit(string? ssn)
+ {
+ if (string.IsNullOrWhiteSpace(ssn))
+ {
+ return false;
+ }
+
+ string cleaned = ssn.Trim().ToUpper();
+ return cleaned.Length == 18 && SSN18Regex.IsMatch(cleaned) && ValidateCheckDigit(cleaned);
+ }
+
+ ///
+ /// 验证格式是否正确(不校验校验位)
+ ///
+ /// 社保号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? ssn)
+ {
+ if (string.IsNullOrWhiteSpace(ssn))
+ {
+ return false;
+ }
+
+ string cleaned = ssn.Trim().ToUpper();
+ return SSN18Regex.IsMatch(cleaned) || SSN15Regex.IsMatch(cleaned) || SSNCardRegex.IsMatch(cleaned);
+ }
+
+ ///
+ /// 验证校验位
+ ///
+ private static bool ValidateCheckDigit(string ssn)
+ {
+ if (ssn.Length != 18) return false;
+
+ int sum = 0;
+ for (int i = 0; i < 17; i++)
+ {
+ sum += (ssn[i] - '0') * Weights[i];
+ }
+
+ char expectedCheckCode = CheckCodes[sum % 11];
+ return char.ToUpper(ssn[17]) == expectedCheckCode;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取出生日期(仅18位格式)
+ ///
+ /// 社保号
+ /// 出生日期
+ public static DateTime? GetBirthday(string? ssn)
+ {
+ if (!Is18Digit(ssn))
+ {
+ return null;
+ }
+
+ string cleaned = ssn!.Trim();
+ int year = int.Parse(cleaned.Substring(6, 4));
+ int month = int.Parse(cleaned.Substring(10, 2));
+ int day = int.Parse(cleaned.Substring(12, 2));
+
+ return new DateTime(year, month, day);
+ }
+
+ ///
+ /// 获取性别(仅18位格式)
+ ///
+ /// 社保号
+ /// 性别(1男2女)
+ public static int? GetGender(string? ssn)
+ {
+ if (!Is18Digit(ssn))
+ {
+ return null;
+ }
+
+ int genderDigit = ssn![16] - '0';
+ return genderDigit % 2 == 1 ? 1 : 2;
+ }
+
+ ///
+ /// 获取性别字符串
+ ///
+ /// 社保号
+ /// 性别
+ public static string? GetGenderString(string? ssn)
+ {
+ int? gender = GetGender(ssn);
+ return gender switch
+ {
+ 1 => "男",
+ 2 => "女",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取行政区划代码(仅18位格式)
+ ///
+ /// 社保号
+ /// 行政区划代码
+ public static string? GetAreaCode(string? ssn)
+ {
+ if (!Is18Digit(ssn))
+ {
+ return null;
+ }
+
+ return ssn!.Substring(0, 6);
+ }
+
+ ///
+ /// 获取年龄(仅18位格式)
+ ///
+ /// 社保号
+ /// 年龄
+ public static int? GetAge(string? ssn)
+ {
+ DateTime? birthday = GetBirthday(ssn);
+ if (!birthday.HasValue)
+ {
+ return null;
+ }
+
+ DateTime today = DateTime.Today;
+ int age = today.Year - birthday.Value.Year;
+ if (today < birthday.Value.AddYears(age))
+ {
+ age--;
+ }
+
+ return age;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化社保号
+ ///
+ /// 社保号
+ /// 格式化后的社保号
+ public static string? Normalize(string? ssn)
+ {
+ if (string.IsNullOrWhiteSpace(ssn))
+ {
+ return null;
+ }
+
+ string cleaned = ssn.Trim().ToUpper();
+ return IsValidFormat(cleaned) ? cleaned : null;
+ }
+
+ ///
+ /// 社保号脱敏:110***********1234
+ ///
+ /// 社保号
+ /// 脱敏后的社保号
+ public static string? Mask(string? ssn)
+ {
+ if (!IsValid(ssn))
+ {
+ return null;
+ }
+
+ string cleaned = ssn!.Trim().ToUpper();
+
+ if (cleaned.Length == 18)
+ {
+ return cleaned.Substring(0, 3) + "***********" + cleaned.Substring(14);
+ }
+
+ if (cleaned.Length >= 15)
+ {
+ int prefixLen = 3;
+ int suffixLen = 4;
+ return cleaned.Substring(0, prefixLen) +
+ new string('*', cleaned.Length - prefixLen - suffixLen) +
+ cleaned.Substring(cleaned.Length - suffixLen);
+ }
+
+ return cleaned[0] + new string('*', cleaned.Length - 2) + cleaned[^1];
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/StockCodeUtil.cs b/EasyTool.Core/BusinessCategory/StockCodeUtil.cs
new file mode 100644
index 0000000..a2d7122
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/StockCodeUtil.cs
@@ -0,0 +1,529 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 股票市场枚举
+ ///
+ public enum StockMarket
+ {
+ ///
+ /// 未知
+ ///
+ Unknown = 0,
+
+ ///
+ /// 上海证券交易所
+ ///
+ SHSE = 1,
+
+ ///
+ /// 深圳证券交易所
+ ///
+ SZSE = 2,
+
+ ///
+ /// 北京证券交易所
+ ///
+ BSE = 3,
+
+ ///
+ /// 香港交易所
+ ///
+ HKEX = 4,
+
+ ///
+ /// 纽约证券交易所
+ ///
+ NYSE = 5,
+
+ ///
+ /// 纳斯达克
+ ///
+ NASDAQ = 6
+ }
+
+ ///
+ /// 股票类型枚举
+ ///
+ public enum StockType
+ {
+ ///
+ /// 未知
+ ///
+ Unknown = 0,
+
+ ///
+ /// A股
+ ///
+ AShare = 1,
+
+ ///
+ /// B股
+ ///
+ BShare = 2,
+
+ ///
+ /// 创业板
+ ///
+ ChiNext = 3,
+
+ ///
+ /// 科创板
+ ///
+ STAR = 4,
+
+ ///
+ /// 北交所
+ ///
+ BSEShare = 5,
+
+ ///
+ /// 港股
+ ///
+ HKStock = 6,
+
+ ///
+ /// 美股
+ ///
+ USStock = 7
+ }
+
+ ///
+ /// 股票代码工具类
+ ///
+ public static class StockCodeUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// A股代码正则表达式(6位数字)
+ ///
+ private static readonly Regex AShareRegex = new(@"^[036]\d{5}$", RegexOptions.Compiled);
+
+ ///
+ /// B股代码正则表达式
+ ///
+ private static readonly Regex BShareRegex = new(@"^[29]\d{5}$", RegexOptions.Compiled);
+
+ ///
+ /// 创业板代码正则表达式(30开头)
+ ///
+ private static readonly Regex ChiNextRegex = new(@"^30\d{4}$", RegexOptions.Compiled);
+
+ ///
+ /// 科创板代码正则表达式(688开头)
+ ///
+ private static readonly Regex STARRegex = new(@"^688\d{3}$", RegexOptions.Compiled);
+
+ ///
+ /// 北交所代码正则表达式(8开头,4位或6位)
+ ///
+ private static readonly Regex BSERegex = new(@"^(8[34]\d{4}|4[38]\d{4})$", RegexOptions.Compiled);
+
+ ///
+ /// 港股代码正则表达式(1-5位数字)
+ ///
+ private static readonly Regex HKStockRegex = new(@"^\d{4,5}$", RegexOptions.Compiled);
+
+ ///
+ /// 美股代码正则表达式(1-5位大写字母)
+ ///
+ private static readonly Regex USStockRegex = new(@"^[A-Z]{1,5}$", RegexOptions.Compiled);
+
+ ///
+ /// 常见A股股票代码映射(部分示例)
+ ///
+ private static readonly Dictionary StockCodeMap = new()
+ {
+ // 上证A股
+ { "600000", ("浦发银行", "上海") }, { "600036", ("招商银行", "上海") },
+ { "600519", ("贵州茅台", "上海") }, { "600887", ("伊利股份", "上海") },
+ { "601318", ("中国平安", "上海") }, { "601398", ("工商银行", "上海") },
+ { "601939", ("建设银行", "上海") }, { "601988", ("中国银行", "上海") },
+ { "601288", ("农业银行", "上海") }, { "601857", ("中国石油", "上海") },
+ { "601668", ("中国建筑", "上海") }, { "600276", ("恒瑞医药", "上海") },
+ { "600309", ("万华化学", "上海") }, { "600900", ("长江电力", "上海") },
+ { "601012", ("隆基绿能", "上海") }, { "603259", ("药明康德", "上海") },
+
+ // 深证A股
+ { "000001", ("平安银行", "深圳") }, { "000002", ("万科A", "深圳") },
+ { "000333", ("美的集团", "深圳") }, { "000651", ("格力电器", "深圳") },
+ { "000858", ("五粮液", "深圳") }, { "002594", ("比亚迪", "深圳") },
+ { "000063", ("中兴通讯", "深圳") }, { "002475", ("立讯精密", "深圳") },
+ { "002415", ("海康威视", "深圳") }, { "002352", ("顺丰控股", "深圳") },
+ { "000568", ("泸州老窖", "深圳") }, { "002714", ("牧原股份", "深圳") },
+
+ // 创业板
+ { "300750", ("宁德时代", "深圳") }, { "300059", ("东方财富", "深圳") },
+ { "300015", ("爱尔眼科", "深圳") }, { "300347", ("泰格医药", "深圳") },
+ { "300760", ("迈瑞医疗", "深圳") }, { "300124", ("汇川技术", "深圳") },
+
+ // 科创板
+ { "688981", ("中芯国际", "上海") }, { "688111", ("金山办公", "上海") },
+ { "688012", ("中微公司", "上海") }, { "688256", ("寒武纪", "上海") },
+
+ // 港股
+ { "00700", ("腾讯控股", "香港") }, { "09988", ("阿里巴巴-SW", "香港") },
+ { "03690", ("美团-W", "香港") }, { "09999", ("网易-S", "香港") },
+ { "01024", ("快手-W", "香港") }, { "01810", ("小米集团-W", "香港") },
+ { "09618", ("京东集团-SW", "香港") }, { "02318", ("中国平安", "香港") },
+ { "00005", ("汇丰控股", "香港") }, { "00941", ("中国移动", "香港") },
+ { "03988", ("中国银行", "香港") }, { "01398", ("工商银行", "香港") },
+
+ // 美股
+ { "AAPL", ("苹果", "纳斯达克") }, { "MSFT", ("微软", "纳斯达克") },
+ { "GOOGL", ("谷歌", "纳斯达克") }, { "AMZN", ("亚马逊", "纳斯达克") },
+ { "META", ("Meta", "纳斯达克") }, { "NVDA", ("英伟达", "纳斯达克") },
+ { "TSLA", ("特斯拉", "纳斯达克") }, { "NFLX", ("奈飞", "纳斯达克") },
+ { "BABA", ("阿里巴巴", "纽约") }, { "JD", ("京东", "纳斯达克") },
+ { "PDD", ("拼多多", "纳斯达克") }, { "BIDU", ("百度", "纳斯达克") }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证股票代码是否有效(支持A股、港股、美股)
+ ///
+ /// 股票代码
+ /// 市场类型(可选,默认自动识别)
+ /// 是否有效
+ public static bool IsValid(string? code, StockMarket? market = null)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return false;
+ }
+
+ if (market.HasValue)
+ {
+ return market.Value switch
+ {
+ StockMarket.SHSE or StockMarket.SZSE => IsValidAShare(code),
+ StockMarket.BSE => IsValidBSE(code),
+ StockMarket.HKEX => IsValidHKStock(code),
+ StockMarket.NYSE or StockMarket.NASDAQ => IsValidUSStock(code),
+ _ => false
+ };
+ }
+
+ return IsValidAShare(code) || IsValidBSE(code) || IsValidHKStock(code) || IsValidUSStock(code);
+ }
+
+ ///
+ /// 验证A股代码是否有效
+ ///
+ /// 股票代码
+ /// 是否有效
+ public static bool IsValidAShare(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code) || code.Length != 6)
+ {
+ return false;
+ }
+
+ // A股(60、00、30、688开头)和B股(20、900开头)
+ return AShareRegex.IsMatch(code) || BShareRegex.IsMatch(code) ||
+ ChiNextRegex.IsMatch(code) || STARRegex.IsMatch(code);
+ }
+
+ ///
+ /// 验证北交所代码是否有效
+ ///
+ /// 股票代码
+ /// 是否有效
+ public static bool IsValidBSE(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code) || code.Length != 6)
+ {
+ return false;
+ }
+
+ return BSERegex.IsMatch(code);
+ }
+
+ ///
+ /// 验证港股代码是否有效
+ ///
+ /// 股票代码
+ /// 是否有效
+ public static bool IsValidHKStock(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return false;
+ }
+
+ return HKStockRegex.IsMatch(code);
+ }
+
+ ///
+ /// 验证美股代码是否有效
+ ///
+ /// 股票代码
+ /// 是否有效
+ public static bool IsValidUSStock(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return false;
+ }
+
+ return USStockRegex.IsMatch(code.ToUpper());
+ }
+
+ #endregion
+
+ #region 市场识别
+
+ ///
+ /// 获取股票市场
+ ///
+ /// 股票代码
+ /// 股票市场
+ public static StockMarket GetMarket(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return StockMarket.Unknown;
+ }
+
+ string upper = code.ToUpper();
+
+ // 美股(字母代码)
+ if (USStockRegex.IsMatch(upper))
+ {
+ return StockMarket.NASDAQ; // 简化处理
+ }
+
+ // 港股(4-5位数字)
+ if (HKStockRegex.IsMatch(code))
+ {
+ return StockMarket.HKEX;
+ }
+
+ // A股(6位数字)
+ if (code.Length == 6)
+ {
+ if (code.StartsWith("60") || code.StartsWith("68"))
+ {
+ return StockMarket.SHSE;
+ }
+ if (code.StartsWith("00") || code.StartsWith("30"))
+ {
+ return StockMarket.SZSE;
+ }
+ if (code.StartsWith("83") || code.StartsWith("87") || code.StartsWith("43") || code.StartsWith("83"))
+ {
+ return StockMarket.BSE;
+ }
+ // B股
+ if (code.StartsWith("900"))
+ {
+ return StockMarket.SHSE;
+ }
+ if (code.StartsWith("200"))
+ {
+ return StockMarket.SZSE;
+ }
+ }
+
+ return StockMarket.Unknown;
+ }
+
+ ///
+ /// 获取股票市场名称
+ ///
+ /// 股票市场
+ /// 市场名称
+ public static string GetMarketName(StockMarket market)
+ {
+ return market switch
+ {
+ StockMarket.SHSE => "上海证券交易所",
+ StockMarket.SZSE => "深圳证券交易所",
+ StockMarket.BSE => "北京证券交易所",
+ StockMarket.HKEX => "香港交易所",
+ StockMarket.NYSE => "纽约证券交易所",
+ StockMarket.NASDAQ => "纳斯达克",
+ _ => "未知"
+ };
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取股票类型
+ ///
+ /// 股票代码
+ /// 股票类型
+ public static StockType GetStockType(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return StockType.Unknown;
+ }
+
+ string upper = code.ToUpper();
+
+ // 美股
+ if (USStockRegex.IsMatch(upper))
+ {
+ return StockType.USStock;
+ }
+
+ // 港股
+ if (HKStockRegex.IsMatch(code))
+ {
+ return StockType.HKStock;
+ }
+
+ // A股细分
+ if (code.Length == 6)
+ {
+ if (STARRegex.IsMatch(code)) return StockType.STAR;
+ if (ChiNextRegex.IsMatch(code)) return StockType.ChiNext;
+ if (BSERegex.IsMatch(code)) return StockType.BSEShare;
+ if (code.StartsWith("60") || code.StartsWith("00")) return StockType.AShare;
+ if (code.StartsWith("900") || code.StartsWith("200")) return StockType.BShare;
+ }
+
+ return StockType.Unknown;
+ }
+
+ ///
+ /// 获取股票类型名称
+ ///
+ /// 股票类型
+ /// 类型名称
+ public static string GetStockTypeName(StockType type)
+ {
+ return type switch
+ {
+ StockType.AShare => "A股",
+ StockType.BShare => "B股",
+ StockType.ChiNext => "创业板",
+ StockType.STAR => "科创板",
+ StockType.BSEShare => "北交所",
+ StockType.HKStock => "港股",
+ StockType.USStock => "美股",
+ _ => "未知"
+ };
+ }
+
+ #endregion
+
+ #region 信息查询
+
+ ///
+ /// 获取股票名称
+ ///
+ /// 股票代码
+ /// 股票名称
+ public static string? GetName(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return null;
+ }
+
+ string key = code.ToUpper().PadLeft(6, '0');
+ if (StockCodeMap.TryGetValue(key, out var info))
+ {
+ return info.Name;
+ }
+
+ // 尝试原始格式
+ if (StockCodeMap.TryGetValue(code.ToUpper(), out info))
+ {
+ return info.Name;
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取完整股票代码(带市场前缀)
+ ///
+ /// 股票代码
+ /// 完整代码(如sh600519、sz000001、hk00700)
+ public static string? GetFullCode(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return null;
+ }
+
+ StockMarket market = GetMarket(code);
+ return market switch
+ {
+ StockMarket.SHSE => "sh" + code,
+ StockMarket.SZSE => "sz" + code,
+ StockMarket.BSE => "bj" + code,
+ StockMarket.HKEX => "hk" + code.PadLeft(5, '0'),
+ StockMarket.NYSE => "nyse:" + code.ToUpper(),
+ StockMarket.NASDAQ => "nasdaq:" + code.ToUpper(),
+ _ => null
+ };
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化股票代码
+ ///
+ /// 股票代码
+ /// 格式化后的代码
+ public static string? Normalize(string? code)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ return null;
+ }
+
+ // 去除市场前缀
+ string cleaned = code.ToLower()
+ .Replace("sh", "").Replace("sz", "").Replace("bj", "")
+ .Replace("hk", "").Replace("nyse:", "").Replace("nasdaq:", "");
+
+ // 港股补零
+ if (HKStockRegex.IsMatch(cleaned) && cleaned.Length < 5)
+ {
+ cleaned = cleaned.PadLeft(5, '0');
+ }
+
+ return IsValid(cleaned) ? cleaned.ToUpper() : null;
+ }
+
+ ///
+ /// 股票代码脱敏:60****9
+ ///
+ /// 股票代码
+ /// 脱敏后的代码
+ public static string? Mask(string? code)
+ {
+ string? normalized = Normalize(code);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ if (normalized.Length <= 2)
+ {
+ return normalized[0] + "*";
+ }
+
+ return normalized[0] + new string('*', normalized.Length - 2) + normalized[^1];
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/SwiftCodeUtil.cs b/EasyTool.Core/BusinessCategory/SwiftCodeUtil.cs
new file mode 100644
index 0000000..3709510
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/SwiftCodeUtil.cs
@@ -0,0 +1,438 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// SWIFT银行代码工具类
+ ///
+ public static class SwiftCodeUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// SWIFT代码正则表达式(8位或11位)
+ ///
+ private static readonly Regex SwiftRegex = new(
+ @"^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 中国主要银行SWIFT代码映射
+ ///
+ private static readonly Dictionary ChinaBankSwiftMap = new()
+ {
+ // 工商银行
+ { "ICBKCNBJ", ("中国工商银行", "北京") },
+ { "ICBKCNBJBJN", ("中国工商银行", "济南") },
+ { "ICBKCNBJCQX", ("中国工商银行", "重庆") },
+ { "ICBKCNBJSHI", ("中国工商银行", "上海") },
+ { "ICBKCNBJSZN", ("中国工商银行", "深圳") },
+ { "ICBKCNBJGZU", ("中国工商银行", "广州") },
+ { "ICBKCNBJNJA", ("中国工商银行", "南京") },
+ { "ICBKCNBJHBR", ("中国工商银行", "哈尔滨") },
+ { "ICBKCNBJTJN", ("中国工商银行", "天津") },
+ { "ICBKCNBJCDU", ("中国工商银行", "成都") },
+ { "ICBKCNBJWUH", ("中国工商银行", "武汉") },
+ { "ICBKCNBJHAN", ("中国工商银行", "杭州") },
+ { "ICBKCNBJXIM", ("中国工商银行", "厦门") },
+ { "ICBKCNBJDLC", ("中国工商银行", "大连") },
+ { "ICBKCNBJSYN", ("中国工商银行", "沈阳") },
+ { "ICBKCNBJJIX", ("中国工商银行", "吉林") },
+ { "ICBKCNBJSWA", ("中国工商银行", "汕头") },
+ { "ICBKCNBJZHO", ("中国工商银行", "珠海") },
+ { "ICBKCNBJFZH", ("中国工商银行", "福州") },
+ { "ICBKCNBJKUN", ("中国工商银行", "昆明") },
+
+ // 农业银行
+ { "ABOCCNBJ", ("中国农业银行", "北京") },
+ { "ABOCCNBJ070", ("中国农业银行", "哈尔滨") },
+ { "ABOCCNBJ080", ("中国农业银行", "上海") },
+ { "ABOCCNBJ100", ("中国农业银行", "广州") },
+ { "ABOCCNBJ110", ("中国农业银行", "深圳") },
+ { "ABOCCNBJ120", ("中国农业银行", "天津") },
+ { "ABOCCNBJ130", ("中国农业银行", "重庆") },
+ { "ABOCCNBJ140", ("中国农业银行", "南京") },
+ { "ABOCCNBJ150", ("中国农业银行", "成都") },
+ { "ABOCCNBJ160", ("中国农业银行", "武汉") },
+ { "ABOCCNBJ170", ("中国农业银行", "杭州") },
+ { "ABOCCNBJ180", ("中国农业银行", "济南") },
+ { "ABOCCNBJ190", ("中国农业银行", "西安") },
+ { "ABOCCNBJ200", ("中国农业银行", "沈阳") },
+
+ // 中国银行
+ { "BKCHCNBJ", ("中国银行", "北京") },
+ { "BKCHCNBJ300", ("中国银行", "上海") },
+ { "BKCHCNBJ400", ("中国银行", "广州") },
+ { "BKCHCNBJ500", ("中国银行", "深圳") },
+ { "BKCHCNBJ600", ("中国银行", "天津") },
+ { "BKCHCNBJ700", ("中国银行", "重庆") },
+ { "BKCHCNBJ800", ("中国银行", "南京") },
+ { "BKCHCNBJ900", ("中国银行", "成都") },
+ { "BKCHCNBJ910", ("中国银行", "武汉") },
+ { "BKCHCNBJ920", ("中国银行", "杭州") },
+ { "BKCHCNBJ930", ("中国银行", "济南") },
+ { "BKCHCNBJ940", ("中国银行", "西安") },
+ { "BKCHCNBJ950", ("中国银行", "沈阳") },
+ { "BKCHCNBJ960", ("中国银行", "大连") },
+ { "BKCHCNBJ970", ("中国银行", "青岛") },
+ { "BKCHCNBJ980", ("中国银行", "厦门") },
+ { "BKCHCNBJ990", ("中国银行", "福州") },
+
+ // 建设银行
+ { "PCBCCNBJ", ("中国建设银行", "北京") },
+ { "PCBCCNBJBJX", ("中国建设银行", "北京") },
+ { "PCBCCNBJSHX", ("中国建设银行", "上海") },
+ { "PCBCCNBJGZX", ("中国建设银行", "广州") },
+ { "PCBCCNBJSZX", ("中国建设银行", "深圳") },
+ { "PCBCCNBJTJX", ("中国建设银行", "天津") },
+ { "PCBCCNBJCQX", ("中国建设银行", "重庆") },
+ { "PCBCCNBJNJX", ("中国建设银行", "南京") },
+ { "PCBCCNBJCDX", ("中国建设银行", "成都") },
+ { "PCBCCNBJWHX", ("中国建设银行", "武汉") },
+ { "PCBCCNBJHZX", ("中国建设银行", "杭州") },
+ { "PCBCCNBJJNX", ("中国建设银行", "济南") },
+ { "PCBCCNBJXAX", ("中国建设银行", "西安") },
+ { "PCBCCNBJSYX", ("中国建设银行", "沈阳") },
+ { "PCBCCNBJDLX", ("中国建设银行", "大连") },
+ { "PCBCCNBJQDX", ("中国建设银行", "青岛") },
+
+ // 交通银行
+ { "COMMCNSh", ("交通银行", "上海") },
+ { "COMMCNShKUN", ("交通银行", "昆明") },
+ { "COMMCNShGZH", ("交通银行", "广州") },
+
+ // 招商银行
+ { "CMBCCNBS", ("招商银行", "上海") },
+ { "CMBCCNBS001", ("招商银行", "上海") },
+ { "CMBCCNBS002", ("招商银行", "北京") },
+ { "CMBCCNBS003", ("招商银行", "深圳") },
+ { "CMBCCNBS004", ("招商银行", "广州") },
+
+ // 中信银行
+ { "CIBKCNBJ", ("中信银行", "北京") },
+ { "CIBKCNBJSHI", ("中信银行", "上海") },
+ { "CIBKCNBJGZU", ("中信银行", "广州") },
+ { "CIBKCNBJSZN", ("中信银行", "深圳") },
+
+ // 浦发银行
+ { "SPDBCNSH", ("浦发银行", "上海") },
+ { "SPDBCNSHBJG", ("浦发银行", "北京") },
+ { "SPDBCNSHGXG", ("浦发银行", "广州") },
+ { "SPDBCNSHSZN", ("浦发银行", "深圳") },
+
+ // 民生银行
+ { "MSBCCNBJ", ("民生银行", "北京") },
+ { "MSBCCNBJ001", ("民生银行", "上海") },
+ { "MSBCCNBJ002", ("民生银行", "广州") },
+
+ // 光大银行
+ { "EVERCNBJ", ("光大银行", "北京") },
+ { "EVERCNBJ1BJ", ("光大银行", "北京") },
+ { "EVERCNBJ1SH", ("光大银行", "上海") },
+
+ // 华夏银行
+ { "HXBKCNBJ", ("华夏银行", "北京") },
+ { "HXBKCNBJ070", ("华夏银行", "上海") },
+
+ // 兴业银行
+ { "FJIBCNBA", ("兴业银行", "福州") },
+ { "FJIBCNBA001", ("兴业银行", "北京") },
+ { "FJIBCNBA002", ("兴业银行", "上海") },
+
+ // 平安银行
+ { "SZDBCNBS", ("平安银行", "深圳") },
+ { "SZDBCNBS001", ("平安银行", "北京") },
+ { "SZDBCNBS002", ("平安银行", "上海") },
+
+ // 广发银行
+ { "GDBKCN22", ("广发银行", "广州") },
+ { "GDBKCN22001", ("广发银行", "北京") },
+ { "GDBKCN22002", ("广发银行", "上海") },
+
+ // 邮储银行
+ { "PSBCCNBJ", ("邮储银行", "北京") },
+ { "PSBCCNBJ001", ("邮储银行", "上海") },
+ { "PSBCCNBJ002", ("邮储银行", "广州") },
+
+ // 汇丰银行(中国)
+ { "HSBCCNSH", ("汇丰银行(中国)", "上海") },
+ { "HSBCCNSH001", ("汇丰银行(中国)", "北京") },
+ { "HSBCCNSH002", ("汇丰银行(中国)", "广州") },
+
+ // 渣打银行(中国)
+ { "SCBLCNSX", ("渣打银行(中国)", "上海") },
+ { "SCBLCNSX001", ("渣打银行(中国)", "北京") },
+
+ // 花旗银行(中国)
+ { "CITICNSX", ("花旗银行(中国)", "上海") },
+ { "CITICNSX001", ("花旗银行(中国)", "北京") }
+ };
+
+ ///
+ /// 国家代码与名称映射(部分)
+ ///
+ private static readonly Dictionary CountryCodeMap = new()
+ {
+ { "CN", "中国" }, { "HK", "香港" }, { "TW", "台湾" }, { "JP", "日本" },
+ { "KR", "韩国" }, { "SG", "新加坡" }, { "MY", "马来西亚" }, { "TH", "泰国" },
+ { "AU", "澳大利亚" }, { "NZ", "新西兰" }, { "US", "美国" }, { "CA", "加拿大" },
+ { "GB", "英国" }, { "DE", "德国" }, { "FR", "法国" }, { "IT", "意大利" },
+ { "ES", "西班牙" }, { "NL", "荷兰" }, { "BE", "比利时" }, { "CH", "瑞士" },
+ { "AT", "奥地利" }, { "SE", "瑞典" }, { "NO", "挪威" }, { "DK", "丹麦" },
+ { "FI", "芬兰" }, { "RU", "俄罗斯" }, { "BR", "巴西" }, { "MX", "墨西哥" },
+ { "AR", "阿根廷" }, { "ZA", "南非" }, { "AE", "阿联酋" }, { "SA", "沙特" },
+ { "IN", "印度" }, { "PK", "巴基斯坦" }, { "ID", "印度尼西亚" }, { "PH", "菲律宾" },
+ { "VN", "越南" }, { "MM", "缅甸" }, { "LU", "卢森堡" }, { "IE", "爱尔兰" },
+ { "PT", "葡萄牙" }, { "GR", "希腊" }, { "PL", "波兰" }, { "CZ", "捷克" },
+ { "HU", "匈牙利" }, { "TR", "土耳其" }, { "IL", "以色列" }, { "EG", "埃及" }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证SWIFT代码是否有效
+ ///
+ /// SWIFT代码
+ /// 是否有效
+ public static bool IsValid(string? swiftCode)
+ {
+ if (string.IsNullOrWhiteSpace(swiftCode))
+ {
+ return false;
+ }
+
+ return SwiftRegex.IsMatch(swiftCode);
+ }
+
+ ///
+ /// 验证是否为8位SWIFT代码(不含分行代码)
+ ///
+ /// SWIFT代码
+ /// 是否为8位
+ public static bool Is8Digit(string? swiftCode)
+ {
+ return swiftCode?.Length == 8 && IsValid(swiftCode);
+ }
+
+ ///
+ /// 验证是否为11位SWIFT代码(含分行代码)
+ ///
+ /// SWIFT代码
+ /// 是否为11位
+ public static bool Is11Digit(string? swiftCode)
+ {
+ return swiftCode?.Length == 11 && IsValid(swiftCode);
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取银行代码(前4位)
+ ///
+ /// SWIFT代码
+ /// 银行代码
+ public static string? GetBankCode(string? swiftCode)
+ {
+ if (!IsValid(swiftCode))
+ {
+ return null;
+ }
+
+ return swiftCode!.Substring(0, 4).ToUpper();
+ }
+
+ ///
+ /// 获取国家代码(第5-6位)
+ ///
+ /// SWIFT代码
+ /// 国家代码
+ public static string? GetCountryCode(string? swiftCode)
+ {
+ if (!IsValid(swiftCode))
+ {
+ return null;
+ }
+
+ return swiftCode!.Substring(4, 2).ToUpper();
+ }
+
+ ///
+ /// 获取国家名称
+ ///
+ /// SWIFT代码
+ /// 国家名称
+ public static string? GetCountryName(string? swiftCode)
+ {
+ string? countryCode = GetCountryCode(swiftCode);
+ if (countryCode == null)
+ {
+ return null;
+ }
+
+ return CountryCodeMap.TryGetValue(countryCode, out string? name) ? name : null;
+ }
+
+ ///
+ /// 获取位置代码(第7-8位)
+ ///
+ /// SWIFT代码
+ /// 位置代码
+ public static string? GetLocationCode(string? swiftCode)
+ {
+ if (!IsValid(swiftCode))
+ {
+ return null;
+ }
+
+ return swiftCode!.Substring(6, 2).ToUpper();
+ }
+
+ ///
+ /// 获取分行代码(第9-11位,11位代码才有)
+ ///
+ /// SWIFT代码
+ /// 分行代码
+ public static string? GetBranchCode(string? swiftCode)
+ {
+ if (!Is11Digit(swiftCode))
+ {
+ return null;
+ }
+
+ return swiftCode!.Substring(8, 3).ToUpper();
+ }
+
+ ///
+ /// 判断是否为总行代码(第7-8位为XX或位置代码首位为0)
+ ///
+ /// SWIFT代码
+ /// 是否为总行
+ public static bool IsHeadOffice(string? swiftCode)
+ {
+ string? locationCode = GetLocationCode(swiftCode);
+ if (locationCode == null)
+ {
+ return false;
+ }
+
+ // 位置代码为"XX"或首位为0表示总行
+ return locationCode == "XX" || locationCode[0] == '0';
+ }
+
+ ///
+ /// 获取银行信息(仅限中国主要银行)
+ ///
+ /// SWIFT代码
+ /// 银行和城市信息
+ public static (string Bank, string City)? GetBankInfo(string? swiftCode)
+ {
+ if (!IsValid(swiftCode))
+ {
+ return null;
+ }
+
+ string upper = swiftCode!.ToUpper();
+
+ // 先尝试完整匹配
+ if (ChinaBankSwiftMap.TryGetValue(upper, out var info))
+ {
+ return info;
+ }
+
+ // 再尝试8位匹配
+ string code8 = upper.Substring(0, 8);
+ if (ChinaBankSwiftMap.TryGetValue(code8, out info))
+ {
+ return info;
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取银行名称
+ ///
+ /// SWIFT代码
+ /// 银行名称
+ public static string? GetBankName(string? swiftCode)
+ {
+ return GetBankInfo(swiftCode)?.Bank;
+ }
+
+ ///
+ /// 获取城市名称
+ ///
+ /// SWIFT代码
+ /// 城市名称
+ public static string? GetCityName(string? swiftCode)
+ {
+ return GetBankInfo(swiftCode)?.City;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化SWIFT代码(转大写)
+ ///
+ /// SWIFT代码
+ /// 格式化后的SWIFT代码
+ public static string? Normalize(string? swiftCode)
+ {
+ if (string.IsNullOrWhiteSpace(swiftCode))
+ {
+ return null;
+ }
+
+ string upper = swiftCode.ToUpper().Trim();
+ return IsValid(upper) ? upper : null;
+ }
+
+ ///
+ /// SWIFT代码脱敏:ICBK****BJ
+ ///
+ /// SWIFT代码
+ /// 脱敏后的SWIFT代码
+ public static string? Mask(string? swiftCode)
+ {
+ if (!IsValid(swiftCode))
+ {
+ return null;
+ }
+
+ string upper = swiftCode!.ToUpper();
+ if (upper.Length == 8)
+ {
+ return upper.Substring(0, 4) + "****";
+ }
+ else
+ {
+ return upper.Substring(0, 4) + "*******";
+ }
+ }
+
+ ///
+ /// 转换为8位SWIFT代码(去除分行代码)
+ ///
+ /// SWIFT代码
+ /// 8位SWIFT代码
+ public static string? To8Digit(string? swiftCode)
+ {
+ if (!IsValid(swiftCode))
+ {
+ return null;
+ }
+
+ return swiftCode!.Substring(0, 8).ToUpper();
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/TaxNumberUtil.cs b/EasyTool.Core/BusinessCategory/TaxNumberUtil.cs
new file mode 100644
index 0000000..4ffa424
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/TaxNumberUtil.cs
@@ -0,0 +1,557 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 税号类型枚举
+ ///
+ public enum TaxNumberType
+ {
+ ///
+ /// 未知类型
+ ///
+ Unknown = 0,
+
+ ///
+ /// 统一社会信用代码(18位)
+ ///
+ CreditCode = 1,
+
+ ///
+ /// 旧税号(15位)
+ ///
+ OldTaxCode = 2,
+
+ ///
+ /// 税务登记号(20位)
+ ///
+ TaxRegistration = 3
+ }
+
+ ///
+ /// 企业税号工具类
+ ///
+ public static class TaxNumberUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 统一社会信用代码字符集(31个字符)
+ ///
+ private const string BaseCode = "0123456789ABCDEFGHJKLMNPQRTUWXY";
+
+ ///
+ /// 统一社会信用代码权重
+ ///
+ private static readonly int[] Weights = { 1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28 };
+
+ ///
+ /// 18位统一社会信用代码正则表达式
+ ///
+ private static readonly Regex CreditCodeRegex = new Regex(
+ @"^[0-9A-HJ-NPQRTUWXY]{2}[0-9]{6}[0-9A-HJ-NPQRTUWXY]{10}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 15位旧税号正则表达式(6位区域码 + 9位组织机构代码)
+ ///
+ private static readonly Regex OldTaxCodeRegex = new Regex(
+ @"^[0-9]{15}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 20位税务登记号正则表达式
+ ///
+ private static readonly Regex TaxRegistrationRegex = new Regex(
+ @"^[0-9]{20}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 登记管理部门代码映射
+ ///
+ private static readonly Dictionary DepartmentMap = new Dictionary
+ {
+ { "11", "机构编制" }, { "12", "外交" }, { "13", "教育" }, { "14", "公安" },
+ { "15", "民政" }, { "16", "司法" }, { "17", "交通运输" }, { "18", "文化和旅游" },
+ { "19", "市场监管" }, { "21", "农业" }, { "22", "林业和草原" }, { "23", "卫生健康" },
+ { "24", "中医药" }, { "25", "退役军人" }, { "26", "应急管理" }, { "27", "国有资产" },
+ { "28", "海关" }, { "29", "税务" }, { "31", "人民银行" }, { "32", "外汇" },
+ { "33", "知识产权" }, { "34", "粮食和储备" }, { "35", "能源" }, { "36", "国防科工" },
+ { "37", "烟草" }, { "41", "中央军委" }, { "51", "全国总工会" }, { "52", "全国妇联" },
+ { "53", "全国工商联" }, { "54", "全国青联" }, { "55", "中国残联" },
+ { "91", "工商" }, { "92", "中央及地方编办" }, { "93", "民政" }, { "99", "其他" }
+ };
+
+ ///
+ /// 机构类型代码映射(与登记管理部门组合使用)
+ ///
+ private static readonly Dictionary OrganizationTypeMap = new Dictionary
+ {
+ { '1', "企业" }, { '2', "个体工商户" }, { '3', "农民专业合作社" },
+ { '4', "机关" }, { '5', "事业单位" }, { '6', "社会团体" },
+ { '7', "民办非企业单位" }, { '8', "基金会" }, { '9', "其他" }
+ };
+
+ ///
+ /// 行业代码映射(GB/T 4754-2017 国民经济行业分类,部分常用)
+ ///
+ private static readonly Dictionary IndustryCodeMap = new Dictionary
+ {
+ { "01", "农业" }, { "02", "林业" }, { "03", "畜牧业" }, { "04", "渔业" },
+ { "06", "煤炭开采和洗选业" }, { "07", "石油和天然气开采业" },
+ { "08", "黑色金属矿采选业" }, { "09", "有色金属矿采选业" },
+ { "10", "非金属矿采选业" }, { "13", "农副食品加工业" },
+ { "14", "食品制造业" }, { "15", "酒、饮料和精制茶制造业" },
+ { "17", "纺织业" }, { "18", "纺织服装、服饰业" },
+ { "19", "皮革、毛皮、羽毛及其制品和制鞋业" }, { "20", "木材加工和木、竹、藤、棕、草制品业" },
+ { "21", "家具制造业" }, { "22", "造纸和纸制品业" },
+ { "23", "印刷和记录媒介复制业" }, { "24", "文教、工美、体育和娱乐用品制造业" },
+ { "25", "石油、煤炭及其他燃料加工业" }, { "26", "化学原料和化学制品制造业" },
+ { "27", "医药制造业" }, { "28", "化学纤维制造业" },
+ { "29", "橡胶和塑料制品业" }, { "30", "非金属矿物制品业" },
+ { "31", "黑色金属冶炼和压延加工业" }, { "32", "有色金属冶炼和压延加工业" },
+ { "33", "金属制品业" }, { "34", "通用设备制造业" },
+ { "35", "专用设备制造业" }, { "36", "汽车制造业" },
+ { "37", "铁路、船舶、航空航天和其他运输设备制造业" },
+ { "38", "电气机械和器材制造业" }, { "39", "计算机、通信和其他电子设备制造业" },
+ { "40", "仪器仪表制造业" }, { "41", "其他制造业" },
+ { "42", "废弃资源综合利用业" }, { "43", "金属制品、机械和设备修理业" },
+ { "44", "电力、热力生产和供应业" }, { "45", "燃气生产和供应业" },
+ { "46", "水的生产和供应业" }, { "47", "房屋建筑业" },
+ { "48", "土木工程建筑业" }, { "49", "建筑安装业" },
+ { "50", "建筑装饰、装修和其他建筑业" }, { "51", "批发业" },
+ { "52", "零售业" }, { "53", "铁路运输业" },
+ { "54", "道路运输业" }, { "55", "水上运输业" },
+ { "56", "航空运输业" }, { "57", "管道运输业" },
+ { "58", "多式联运和运输代理业" }, { "59", "装卸搬运和仓储业" },
+ { "60", "邮政业" }, { "61", "住宿业" },
+ { "62", "餐饮业" }, { "63", "电信、广播电视和卫星传输服务" },
+ { "64", "互联网和相关服务" }, { "65", "软件和信息技术服务业" },
+ { "66", "货币金融服务" }, { "67", "资本市场服务" },
+ { "68", "保险业" }, { "69", "其他金融业" },
+ { "70", "房地产业" }, { "71", "租赁业" },
+ { "72", "商务服务业" }, { "73", "研究和试验发展" },
+ { "74", "专业技术服务业" }, { "75", "科技推广和应用服务业" },
+ { "76", "水利管理业" }, { "77", "生态保护和环境治理业" },
+ { "78", "公共设施管理业" }, { "79", "居民服务业" },
+ { "80", "机动车、电子产品和日用产品修理业" }, { "81", "其他服务业" },
+ { "82", "教育" }, { "83", "卫生" },
+ { "84", "社会工作" }, { "85", "新闻和出版业" },
+ { "86", "广播、电视、电影和录音制作业" }, { "87", "文化艺术业" },
+ { "88", "体育" }, { "89", "娱乐业" },
+ { "90", "中国共产党机关" }, { "91", "国家机构" },
+ { "92", "人民政协、民主党派" }, { "93", "社会保障" },
+ { "94", "群众团体、社会团体和其他成员组织" }, { "95", "基层群众自治组织" },
+ { "96", "国际组织" }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证税号是否有效(支持15/18/20位)
+ ///
+ /// 税号
+ /// 是否有效
+ public static bool IsValid(string? taxNumber)
+ {
+ if (string.IsNullOrWhiteSpace(taxNumber))
+ {
+ return false;
+ }
+
+ return IsValid18(taxNumber) || IsValid15(taxNumber) || IsValid20(taxNumber);
+ }
+
+ ///
+ /// 验证18位统一社会信用代码是否有效
+ ///
+ /// 税号
+ /// 是否有效
+ public static bool IsValid18(string? taxNumber)
+ {
+ if (string.IsNullOrWhiteSpace(taxNumber) || taxNumber.Length != 18)
+ {
+ return false;
+ }
+
+ string normalized = taxNumber.ToUpper();
+
+ // 验证格式
+ if (!CreditCodeRegex.IsMatch(normalized))
+ {
+ return false;
+ }
+
+ // 验证校验码
+ char? expectedCheckCode = CalculateCheckCode(normalized.Substring(0, 17));
+ return expectedCheckCode.HasValue && expectedCheckCode.Value == normalized[17];
+ }
+
+ ///
+ /// 验证15位旧税号是否有效
+ ///
+ /// 税号
+ /// 是否有效
+ public static bool IsValid15(string? taxNumber)
+ {
+ if (string.IsNullOrWhiteSpace(taxNumber) || taxNumber.Length != 15)
+ {
+ return false;
+ }
+
+ return OldTaxCodeRegex.IsMatch(taxNumber);
+ }
+
+ ///
+ /// 验证20位税务登记号是否有效
+ ///
+ /// 税号
+ /// 是否有效
+ public static bool IsValid20(string? taxNumber)
+ {
+ if (string.IsNullOrWhiteSpace(taxNumber) || taxNumber.Length != 20)
+ {
+ return false;
+ }
+
+ return TaxRegistrationRegex.IsMatch(taxNumber);
+ }
+
+ ///
+ /// 判断是否为统一社会信用代码(18位)
+ ///
+ /// 税号
+ /// 是否为统一社会信用代码
+ public static bool IsCreditCode(string? taxNumber)
+ {
+ return IsValid18(taxNumber);
+ }
+
+ ///
+ /// 计算统一社会信用代码校验码
+ ///
+ /// 不含校验码的17位代码
+ /// 校验码,计算失败返回null
+ public static char? CalculateCheckCode(string? codeWithoutCheck)
+ {
+ if (string.IsNullOrWhiteSpace(codeWithoutCheck) || codeWithoutCheck.Length != 17)
+ {
+ return null;
+ }
+
+ int sum = 0;
+ for (int i = 0; i < 17; i++)
+ {
+ int value = BaseCode.IndexOf(char.ToUpper(codeWithoutCheck[i]));
+ if (value < 0)
+ {
+ return null;
+ }
+ sum += value * Weights[i];
+ }
+
+ int checkValue = 31 - (sum % 31);
+ if (checkValue == 31)
+ {
+ checkValue = 0;
+ }
+
+ return BaseCode[checkValue];
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取税号类型
+ ///
+ /// 税号
+ /// 税号类型
+ public static TaxNumberType GetTaxNumberType(string? taxNumber)
+ {
+ if (string.IsNullOrWhiteSpace(taxNumber))
+ {
+ return TaxNumberType.Unknown;
+ }
+
+ if (IsValid18(taxNumber))
+ {
+ return TaxNumberType.CreditCode;
+ }
+
+ if (IsValid15(taxNumber))
+ {
+ return TaxNumberType.OldTaxCode;
+ }
+
+ if (IsValid20(taxNumber))
+ {
+ return TaxNumberType.TaxRegistration;
+ }
+
+ return TaxNumberType.Unknown;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取登记管理部门(仅18位统一社会信用代码)
+ ///
+ /// 税号
+ /// 登记管理部门名称
+ public static string? GetDepartment(string? taxNumber)
+ {
+ if (!IsValid18(taxNumber))
+ {
+ return null;
+ }
+
+ string normalized = taxNumber!.ToUpper();
+ string deptCode = normalized.Substring(0, 2);
+
+ return DepartmentMap.TryGetValue(deptCode, out string? dept) ? dept : null;
+ }
+
+ ///
+ /// 获取机构类型(仅18位统一社会信用代码)
+ ///
+ /// 税号
+ /// 机构类型名称
+ public static string? GetOrganizationType(string? taxNumber)
+ {
+ if (!IsValid18(taxNumber))
+ {
+ return null;
+ }
+
+ string normalized = taxNumber!.ToUpper();
+ char typeCode = normalized[2];
+
+ return OrganizationTypeMap.TryGetValue(typeCode, out string? type) ? type : null;
+ }
+
+ ///
+ /// 获取行政区划代码(仅18位统一社会信用代码)
+ ///
+ /// 税号
+ /// 行政区划代码
+ public static string? GetAreaCode(string? taxNumber)
+ {
+ if (!IsValid18(taxNumber))
+ {
+ return null;
+ }
+
+ return taxNumber!.Substring(3, 6);
+ }
+
+ ///
+ /// 获取行业代码(仅18位统一社会信用代码)
+ ///
+ /// 税号
+ /// 行业代码
+ public static string? GetIndustryCode(string? taxNumber)
+ {
+ if (!IsValid18(taxNumber))
+ {
+ return null;
+ }
+
+ return taxNumber!.Substring(9, 2);
+ }
+
+ ///
+ /// 获取行业名称(仅18位统一社会信用代码)
+ ///
+ /// 税号
+ /// 行业名称
+ public static string? GetIndustryName(string? taxNumber)
+ {
+ string? code = GetIndustryCode(taxNumber);
+ if (code == null)
+ {
+ return null;
+ }
+
+ return IndustryCodeMap.TryGetValue(code, out string? name) ? name : null;
+ }
+
+ ///
+ /// 获取主体标识码(仅18位统一社会信用代码)
+ ///
+ /// 税号
+ /// 主体标识码
+ public static string? GetSubjectIdentifier(string? taxNumber)
+ {
+ if (!IsValid18(taxNumber))
+ {
+ return null;
+ }
+
+ return taxNumber!.Substring(11, 6);
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化税号(转大写,去除特殊字符)
+ ///
+ /// 税号
+ /// 格式化后的税号
+ public static string? Normalize(string? taxNumber)
+ {
+ if (string.IsNullOrWhiteSpace(taxNumber))
+ {
+ return null;
+ }
+
+ // 去除空格和特殊字符,转大写
+ return taxNumber.ToUpper().Trim();
+ }
+
+ ///
+ /// 税号脱敏:911010****001Q
+ ///
+ /// 税号
+ /// 脱敏后的税号
+ public static string? Mask(string? taxNumber)
+ {
+ string? normalized = Normalize(taxNumber);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ if (normalized.Length == 18)
+ {
+ // 保留前5位 + 后3位
+ return normalized.Substring(0, 5) + "**********" + normalized.Substring(15);
+ }
+
+ if (normalized.Length == 15)
+ {
+ // 保留前4位 + 后3位
+ return normalized.Substring(0, 4) + "********" + normalized.Substring(12);
+ }
+
+ if (normalized.Length == 20)
+ {
+ // 保留前5位 + 后3位
+ return normalized.Substring(0, 5) + "************" + normalized.Substring(17);
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机统一社会信用代码(仅供测试使用)
+ ///
+ /// 登记管理部门代码(可选,默认91-工商)
+ /// 机构类型代码(可选,默认1-企业)
+ /// 行政区划代码(可选,默认110101-北京市东城区)
+ /// 18位统一社会信用代码
+ public static string GenerateRandom(
+ string? departmentCode = null,
+ char? organizationType = null,
+ string? areaCode = null)
+ {
+ // 登记管理部门代码(2位)
+ string deptCode = departmentCode ?? "91";
+
+ // 机构类型(1位)
+ char orgType = organizationType ?? '1';
+
+ // 行政区划代码(6位)
+ string area = areaCode ?? "110101";
+
+ // 行业代码(2位)
+ string[] industries = { "51", "52", "63", "64", "65", "70", "72" };
+ string industry = MathCategory.RandomUtil.GetRandomElement(industries);
+
+ // 主体标识码(6位)
+ string subject = GenerateRandomCode(6);
+
+ // 前17位
+ string code17 = deptCode + orgType + area + industry + subject;
+
+ // 计算校验码
+ char? checkCode = CalculateCheckCode(code17);
+ if (!checkCode.HasValue)
+ {
+ throw new InvalidOperationException("Failed to calculate check code");
+ }
+
+ return code17 + checkCode.Value;
+ }
+
+ ///
+ /// 将15位旧税号转换为18位统一社会信用代码
+ /// 注意:这是一个近似转换,实际转换需要根据具体情况补充信息
+ ///
+ /// 15位旧税号
+ /// 机构类型代码(默认1-企业)
+ /// 18位统一社会信用代码,转换失败返回null
+ public static string? Convert15To18(string? taxNumber15, char organizationType = '1')
+ {
+ if (!IsValid15(taxNumber15))
+ {
+ return null;
+ }
+
+ // 15位旧税号结构:6位区域码 + 9位组织机构代码
+ // 18位统一社会信用代码结构:
+ // - 登记管理部门(2位):默认91(工商)
+ // - 机构类型(1位)
+ // - 行政区划(6位):取旧税号前6位
+ // - 主体标识码(9位):取旧税号后9位
+ // - 校验码(1位)
+
+ string areaCode = taxNumber15!.Substring(0, 6);
+ string subjectCode = taxNumber15.Substring(6, 9);
+
+ // 前17位:91 + 机构类型 + 区域码 + 主体标识码
+ string code17 = "91" + organizationType + areaCode + subjectCode;
+
+ // 计算校验码
+ char? checkCode = CalculateCheckCode(code17);
+ if (!checkCode.HasValue)
+ {
+ return null;
+ }
+
+ return code17 + checkCode.Value;
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 生成随机代码(使用BaseCode字符集)
+ ///
+ private static string GenerateRandomCode(int length)
+ {
+ string result = "";
+ for (int i = 0; i < length; i++)
+ {
+ result += BaseCode[MathCategory.RandomUtil.RandomInt(0, BaseCode.Length)];
+ }
+ return result;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/TwIdCardUtil.cs b/EasyTool.Core/BusinessCategory/TwIdCardUtil.cs
new file mode 100644
index 0000000..44a17bd
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/TwIdCardUtil.cs
@@ -0,0 +1,357 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 台湾身份证工具类
+ ///
+ public static class TwIdCardUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 台湾身份证正则表达式
+ /// 格式:1个英文字母(县市代码)+ 1位数字(性别)+ 7位数字 + 1位校验码
+ /// 例如:A123456789
+ ///
+ private static readonly Regex TwIdCardRegex = new(
+ @"^[A-Z]\d{9}$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ ///
+ /// 首字母对应数值(台湾身份证特殊编码)
+ ///
+ private static readonly Dictionary LetterValues = new()
+ {
+ { 'A', (10, 0) }, { 'B', (11, 1) }, { 'C', (12, 2) }, { 'D', (13, 3) },
+ { 'E', (14, 4) }, { 'F', (15, 5) }, { 'G', (16, 6) }, { 'H', (17, 7) },
+ { 'I', (34, 4) }, { 'J', (18, 8) }, { 'K', (19, 9) }, { 'L', (20, 0) },
+ { 'M', (21, 1) }, { 'N', (22, 2) }, { 'O', (35, 5) }, { 'P', (23, 3) },
+ { 'Q', (24, 4) }, { 'R', (25, 5) }, { 'S', (26, 6) }, { 'T', (27, 7) },
+ { 'U', (28, 8) }, { 'V', (29, 9) }, { 'W', (32, 2) }, { 'X', (30, 0) },
+ { 'Y', (31, 1) }, { 'Z', (33, 3) }
+ };
+
+ ///
+ /// 县市代码与名称映射
+ ///
+ private static readonly Dictionary CountyMap = new()
+ {
+ { 'A', "台北市" }, { 'B', "台中市" }, { 'C', "基隆市" }, { 'D', "台南市" },
+ { 'E', "高雄市" }, { 'F', "台北县" }, { 'G', "宜兰县" }, { 'H', "桃园县" },
+ { 'I', "嘉义市" }, { 'J', "新竹县" }, { 'K', "苗栗县" }, { 'L', "台中县" },
+ { 'M', "南投县" }, { 'N', "彰化县" }, { 'O', "新竹市" }, { 'P', "云林县" },
+ { 'Q', "嘉义县" }, { 'R', "台南县" }, { 'S', "高雄县" }, { 'T', "屏东县" },
+ { 'U', "花莲县" }, { 'V', "台东县" }, { 'W', "金门县" }, { 'X', "澎湖县" },
+ { 'Y', "阳明山" }, { 'Z', "连江县" }
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证台湾身份证是否有效
+ ///
+ /// 台湾身份证号
+ /// 是否有效
+ public static bool IsValid(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ string cleaned = idCard.ToUpper().Trim();
+
+ // 检查格式
+ if (!TwIdCardRegex.IsMatch(cleaned))
+ {
+ return false;
+ }
+
+ // 检查字母是否有效
+ if (!LetterValues.ContainsKey(cleaned[0]))
+ {
+ return false;
+ }
+
+ // 验证校验码
+ return ValidateCheckDigit(cleaned);
+ }
+
+ ///
+ /// 验证格式是否正确(不校验校验位)
+ ///
+ /// 台湾身份证号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? idCard)
+ {
+ if (string.IsNullOrWhiteSpace(idCard))
+ {
+ return false;
+ }
+
+ string cleaned = idCard.ToUpper().Trim();
+ return TwIdCardRegex.IsMatch(cleaned) && LetterValues.ContainsKey(cleaned[0]);
+ }
+
+ ///
+ /// 验证校验码
+ ///
+ private static bool ValidateCheckDigit(string idCard)
+ {
+ if (idCard.Length != 10)
+ {
+ return false;
+ }
+
+ char letter = char.ToUpper(idCard[0]);
+ if (!LetterValues.TryGetValue(letter, out var values))
+ {
+ return false;
+ }
+
+ // 计算加权和
+ int sum = values.Value1;
+
+ // 第2-9位权重为9到1
+ int[] weights = { 8, 7, 6, 5, 4, 3, 2, 1 };
+ for (int i = 0; i < 8; i++)
+ {
+ sum += (idCard[i + 1] - '0') * weights[i];
+ }
+
+ // 计算校验码
+ int remainder = sum % 10;
+ int expectedCheck = remainder == 0 ? 0 : 10 - remainder;
+
+ int actualCheck = idCard[9] - '0';
+
+ return expectedCheck == actualCheck;
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取县市名称
+ ///
+ /// 台湾身份证号
+ /// 县市名称
+ public static string? GetCounty(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ char letter = char.ToUpper(idCard![0]);
+ return CountyMap.TryGetValue(letter, out string? county) ? county : null;
+ }
+
+ ///
+ /// 获取县市代码(首字母)
+ ///
+ /// 台湾身份证号
+ /// 县市代码
+ public static char? GetCountyCode(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ return char.ToUpper(idCard![0]);
+ }
+
+ ///
+ /// 获取性别
+ ///
+ /// 台湾身份证号
+ /// 性别(1男2女)
+ public static int? GetGender(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ int genderDigit = idCard![1] - '0';
+ // 1为男性,2为女性
+ return genderDigit == 1 ? 1 : (genderDigit == 2 ? 2 : null);
+ }
+
+ ///
+ /// 获取性别字符串
+ ///
+ /// 台湾身份证号
+ /// 性别
+ public static string? GetGenderString(string? idCard)
+ {
+ int? gender = GetGender(idCard);
+ return gender switch
+ {
+ 1 => "男",
+ 2 => "女",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取数字部分(后9位)
+ ///
+ /// 台湾身份证号
+ /// 数字部分
+ public static string? GetDigitPart(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ return idCard!.Substring(1);
+ }
+
+ ///
+ /// 获取校验码(最后一位)
+ ///
+ /// 台湾身份证号
+ /// 校验码
+ public static int? GetCheckDigit(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ return idCard![9] - '0';
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化台湾身份证(统一大写)
+ ///
+ /// 台湾身份证号
+ /// 格式化后的身份证号
+ public static string? Normalize(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ return idCard!.ToUpper().Trim();
+ }
+
+ ///
+ /// 台湾身份证脱敏:A123****89
+ ///
+ /// 台湾身份证号
+ /// 脱敏后的身份证号
+ public static string? Mask(string? idCard)
+ {
+ if (!IsValidFormat(idCard))
+ {
+ return null;
+ }
+
+ string cleaned = idCard!.ToUpper().Trim();
+ // 保留前4位和后2位
+ return cleaned.Substring(0, 4) + "****" + cleaned.Substring(8);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机台湾身份证号(仅供测试使用)
+ ///
+ /// 县市代码(可选,默认随机)
+ /// 性别(1男2女,可选)
+ /// 台湾身份证号
+ public static string GenerateRandom(char? countyCode = null, int? gender = null)
+ {
+ const string letters = "ABCDEFGHJKLMNPQRSTUVXYWZIO";
+ const string digits = "0123456789";
+
+ // 县市代码
+ char letter;
+ if (countyCode.HasValue && LetterValues.ContainsKey(char.ToUpper(countyCode.Value)))
+ {
+ letter = char.ToUpper(countyCode.Value);
+ }
+ else
+ {
+ letter = MathCategory.RandomUtil.GetRandomElement(letters.ToCharArray());
+ }
+
+ // 性别(第2位)
+ int genderDigit;
+ if (gender == 1)
+ {
+ genderDigit = 1;
+ }
+ else if (gender == 2)
+ {
+ genderDigit = 2;
+ }
+ else
+ {
+ genderDigit = MathCategory.RandomUtil.RandomInt(1, 3);
+ }
+
+ // 第3-9位随机数字
+ string middleDigits = "";
+ for (int i = 0; i < 7; i++)
+ {
+ middleDigits += MathCategory.RandomUtil.GetRandomElement(digits.ToCharArray());
+ }
+
+ // 计算校验码
+ string tempId = letter + genderDigit.ToString() + middleDigits + "0";
+ char? checkDigit = CalculateCheckDigit(tempId);
+
+ return $"{letter}{genderDigit}{middleDigits}{checkDigit ?? '0'}";
+ }
+
+ ///
+ /// 计算校验码
+ ///
+ private static char CalculateCheckDigit(string idCard)
+ {
+ if (idCard.Length < 10)
+ {
+ return '0';
+ }
+
+ char letter = char.ToUpper(idCard[0]);
+ if (!LetterValues.TryGetValue(letter, out var values))
+ {
+ return '0';
+ }
+
+ int sum = values.Value1;
+ int[] weights = { 8, 7, 6, 5, 4, 3, 2, 1 };
+
+ for (int i = 0; i < 8; i++)
+ {
+ sum += (idCard[i + 1] - '0') * weights[i];
+ }
+
+ int remainder = sum % 10;
+ int checkValue = remainder == 0 ? 0 : 10 - remainder;
+
+ return (char)('0' + checkValue);
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/VINUtil.cs b/EasyTool.Core/BusinessCategory/VINUtil.cs
new file mode 100644
index 0000000..bfec174
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/VINUtil.cs
@@ -0,0 +1,463 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// VIN(车辆识别代号)工具类
+ ///
+ public static class VINUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// VIN正则表达式(17位,不含I、O、Q)
+ ///
+ private static readonly Regex VINRegex = new(
+ @"^[A-HJ-NPR-Z0-9]{17}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// VIN字符值映射表(不含I、O、Q)
+ ///
+ private static readonly Dictionary CharValueMap = new()
+ {
+ {'A', 1}, {'B', 2}, {'C', 3}, {'D', 4}, {'E', 5}, {'F', 6}, {'G', 7}, {'H', 8},
+ {'J', 1}, {'K', 2}, {'L', 3}, {'M', 4}, {'N', 5}, {'P', 7}, {'R', 9},
+ {'S', 2}, {'T', 3}, {'U', 4}, {'V', 5}, {'W', 6}, {'X', 7}, {'Y', 8}, {'Z', 9},
+ {'0', 0}, {'1', 1}, {'2', 2}, {'3', 3}, {'4', 4}, {'5', 5}, {'6', 6}, {'7', 7}, {'8', 8}, {'9', 9}
+ };
+
+ ///
+ /// VIN位置权重
+ ///
+ private static readonly int[] Weights = { 8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2 };
+
+ ///
+ /// WMI(世界制造商识别码)映射(部分)
+ ///
+ private static readonly (string Code, string Manufacturer)[] WmiMap =
+ {
+ // 中国
+ ("LSV", "上海大众"), ("LSJ", "上海通用"), ("LSG", "上海通用五菱"),
+ ("LDC", "神龙富康"), ("LEN", "北京吉普"), ("LHB", "华晨宝马"),
+ ("LBV", "宝马"), ("LJC", "捷豹路虎"), ("LTV", "天津丰田"),
+ ("LFV", "一汽大众"), ("LFP", "一汽轿车"), ("LFW", "一汽夏利"),
+ ("LKG", "长安铃木"), ("LKL", "长安福特"), ("LLV", "长安汽车"),
+ ("LVF", "东风日产"), ("LUG", "东风本田"), ("LVH", "东风本田"),
+ ("LZW", "柳州五菱"), ("LJD", "江淮汽车"), ("LKY", "奇瑞汽车"),
+ ("LVS", "长安马自达"), ("LZY", "众泰汽车"), ("LVSH", "福特中国"),
+
+ // 德国
+ ("WBA", "宝马"), ("WBS", "宝马M"), ("WBW", "宝马"),
+ ("WAU", "奥迪"), ("WA1", "奥迪SUV"),
+ ("WDB", "奔驰"), ("WDC", "奔驰"), ("WDD", "奔驰"),
+ ("WVW", "大众"), ("WV2", "大众商用车"), ("WVG", "大众SUV"),
+ ("WPO", "保时捷"),
+
+ // 日本
+ ("JTD", "丰田"), ("JTM", "丰田"), ("JTK", "丰田"),
+ ("JHM", "本田"), ("JHG", "本田"), ("JHL", "本田"),
+ ("JN1", "日产"), ("JN8", "日产"), ("JN3", "日产"),
+ ("JM1", "马自达"), ("JMZ", "马自达"),
+ ("JS1", "铃木"), ("JS2", "铃木"), ("JS3", "铃木"),
+ ("KL1", "大宇"), ("KL2", "大宇"),
+
+ // 美国
+ ("1G1", "雪佛兰"), ("1G2", "庞蒂亚克"), ("1G3", "奥兹莫比尔"),
+ ("1G4", "别克"), ("1G6", "凯迪拉克"), ("1G8", "萨博"),
+ ("1GM", "通用"), ("1HG", "本田美国"), ("1J4", "Jeep"),
+ ("1F1", "福特"), ("1F2", "福特"), ("1FA", "福特"), ("1FB", "福特"),
+ ("1C3", "克莱斯勒"), ("1C4", "克莱斯勒"), ("1C6", "克莱斯勒"),
+ ("2G1", "雪佛兰加拿大"), ("2G2", "庞蒂亚克加拿大"),
+ ("2HM", "现代加拿大"), ("2HG", "本田加拿大"),
+
+ // 韩国
+ ("KMH", "现代"), ("KMB", "现代"), ("KNA", "起亚"), ("KND", "起亚"),
+
+ // 英国
+ ("SAJ", "捷豹"), ("SAL", "路虎"), ("SCC", "迈凯伦"),
+
+ // 意大利
+ ("ZAM", "玛莎拉蒂"), ("ZAR", "阿尔法罗密欧"),
+ ("ZDF", "法拉利"), ("ZFF", "法拉利"),
+ ("ZHW", "兰博基尼"),
+
+ // 法国
+ ("VF1", "雷诺"), ("VF3", "标致"), ("VF7", "雪铁龙"),
+
+ // 瑞典
+ ("YV1", "沃尔沃"), ("YV4", "沃尔沃"), ("YV2", "沃尔沃货车")
+ };
+
+ ///
+ /// VDS车辆特征码映射(简化版)
+ ///
+ private static readonly Dictionary VehicleTypeMap = new()
+ {
+ {"A", "轿车"}, {"B", "客车"}, {"C", "跑车"}, {"S", "SUV/跨界车"},
+ {"T", "卡车"}, {"V", "MPV/厢式车"}, {"W", "旅行车"}, {"X", "特种车"}
+ };
+
+ ///
+ /// 年份代码映射
+ ///
+ private static readonly Dictionary YearCodeMap = new()
+ {
+ {'A', 2010}, {'B', 2011}, {'C', 2012}, {'D', 2013}, {'E', 2014},
+ {'F', 2015}, {'G', 2016}, {'H', 2017}, {'J', 2018}, {'K', 2019},
+ {'L', 2020}, {'M', 2021}, {'N', 2022}, {'P', 2023}, {'R', 2024},
+ {'S', 2025}, {'T', 2026}, {'V', 2027}, {'W', 2028}, {'X', 2029},
+ {'Y', 2030},
+ {'1', 2001}, {'2', 2002}, {'3', 2003}, {'4', 2004}, {'5', 2005},
+ {'6', 2006}, {'7', 2007}, {'8', 2008}, {'9', 2009}
+ };
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证VIN是否有效(格式+校验位)
+ ///
+ /// VIN码
+ /// 是否有效
+ public static bool IsValid(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return false;
+ }
+
+ return ValidateCheckDigit(vin!);
+ }
+
+ ///
+ /// 仅验证VIN格式(不校验)
+ ///
+ /// VIN码
+ /// 格式是否正确
+ public static bool IsValidFormat(string? vin)
+ {
+ if (string.IsNullOrWhiteSpace(vin))
+ {
+ return false;
+ }
+
+ return VINRegex.IsMatch(vin.ToUpper());
+ }
+
+ ///
+ /// 验证VIN校验位
+ ///
+ /// VIN码
+ /// 校验位是否正确
+ public static bool ValidateCheckDigit(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return false;
+ }
+
+ string upper = vin!.ToUpper();
+ int sum = 0;
+
+ for (int i = 0; i < 17; i++)
+ {
+ if (!CharValueMap.TryGetValue(upper[i], out int value))
+ {
+ return false;
+ }
+ sum += value * Weights[i];
+ }
+
+ char expectedCheck = (sum % 11) switch
+ {
+ 10 => 'X',
+ _ => (char)('0' + (sum % 11))
+ };
+
+ return upper[8] == expectedCheck;
+ }
+
+ ///
+ /// 计算VIN校验位
+ ///
+ /// 不含校验位的16位VIN
+ /// 校验位(0-9或X),计算失败返回null
+ public static char? CalculateCheckDigit(string? vin16)
+ {
+ if (string.IsNullOrWhiteSpace(vin16) || vin16.Length != 16)
+ {
+ return null;
+ }
+
+ int sum = 0;
+ for (int i = 0; i < 16; i++)
+ {
+ char c = char.ToUpper(vin16[i]);
+ if (!CharValueMap.TryGetValue(c, out int value))
+ {
+ return null;
+ }
+ // 权重需要跳过第9位(校验位位置)
+ int weight = i >= 8 ? Weights[i + 1] : Weights[i];
+ sum += value * weight;
+ }
+
+ return (sum % 11) switch
+ {
+ 10 => 'X',
+ _ => (char)('0' + (sum % 11))
+ };
+ }
+
+ #endregion
+
+ #region 信息提取
+
+ ///
+ /// 获取WMI(世界制造商识别码,前3位)
+ ///
+ /// VIN码
+ /// WMI码
+ public static string? GetWMI(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return null;
+ }
+
+ return vin!.Substring(0, 3).ToUpper();
+ }
+
+ ///
+ /// 获取制造商
+ ///
+ /// VIN码
+ /// 制造商名称
+ public static string? GetManufacturer(string? vin)
+ {
+ string? wmi = GetWMI(vin);
+ if (wmi == null)
+ {
+ return null;
+ }
+
+ foreach (var mapping in WmiMap)
+ {
+ if (wmi.StartsWith(mapping.Code))
+ {
+ return mapping.Manufacturer;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 获取生产地区(根据WMI判断)
+ ///
+ /// VIN码
+ /// 生产地区
+ public static string? GetRegion(string? vin)
+ {
+ string? wmi = GetWMI(vin);
+ if (wmi == null)
+ {
+ return null;
+ }
+
+ char first = wmi[0];
+ return first switch
+ {
+ 'L' => "中国",
+ 'W' => "德国",
+ 'J' => "日本",
+ 'K' => "韩国",
+ '1' or '2' or '3' or '4' or '5' => "美国/加拿大",
+ 'S' => "英国",
+ 'Z' => "意大利",
+ 'V' => "法国",
+ 'Y' => "瑞典",
+ '6' or '7' => "大洋洲",
+ '8' or '9' => "南美洲",
+ _ => null
+ };
+ }
+
+ ///
+ /// 获取VDS(车辆特征码,第4-9位)
+ ///
+ /// VIN码
+ /// VDS码
+ public static string? GetVDS(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return null;
+ }
+
+ return vin!.Substring(3, 6).ToUpper();
+ }
+
+ ///
+ /// 获取VIS(车辆指示码,第10-17位)
+ ///
+ /// VIN码
+ /// VIS码
+ public static string? GetVIS(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return null;
+ }
+
+ return vin!.Substring(9, 8).ToUpper();
+ }
+
+ ///
+ /// 获取车型年份
+ ///
+ /// VIN码
+ /// 车型年份
+ public static int? GetModelYear(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return null;
+ }
+
+ char yearCode = char.ToUpper(vin![9]);
+ return YearCodeMap.TryGetValue(yearCode, out int year) ? year : null;
+ }
+
+ ///
+ /// 获取装配厂代码(第11位)
+ ///
+ /// VIN码
+ /// 装配厂代码
+ public static char? GetPlantCode(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return null;
+ }
+
+ return char.ToUpper(vin![10]);
+ }
+
+ ///
+ /// 获取生产序列号(第12-17位)
+ ///
+ /// VIN码
+ /// 序列号
+ public static string? GetSequenceNumber(string? vin)
+ {
+ if (!IsValidFormat(vin))
+ {
+ return null;
+ }
+
+ return vin!.Substring(11, 6).ToUpper();
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化VIN(转大写)
+ ///
+ /// VIN码
+ /// 格式化后的VIN
+ public static string? Normalize(string? vin)
+ {
+ if (string.IsNullOrWhiteSpace(vin))
+ {
+ return null;
+ }
+
+ string upper = vin.ToUpper().Trim();
+ return upper.Length == 17 && VINRegex.IsMatch(upper) ? upper : null;
+ }
+
+ ///
+ /// VIN脱敏:LSV***********X
+ ///
+ /// VIN码
+ /// 脱敏后的VIN
+ public static string? Mask(string? vin)
+ {
+ string? normalized = Normalize(vin);
+ if (normalized == null)
+ {
+ return null;
+ }
+
+ return normalized.Substring(0, 3) + "*********" + normalized.Substring(14, 3);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机VIN(仅供测试使用)
+ ///
+ /// WMI码(可选,默认LSV-上海大众)
+ /// 车型年份(可选,默认2023)
+ /// 17位VIN
+ public static string GenerateRandom(string? wmi = null, int? modelYear = null)
+ {
+ // WMI(3位)
+ string wmiCode = wmi ?? "LSV";
+
+ // VDS(5位随机)
+ const string vdsChars = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789";
+ string vds = "";
+ for (int i = 0; i < 5; i++)
+ {
+ vds += MathCategory.RandomUtil.GetRandomElement(vdsChars.ToCharArray());
+ }
+
+ // 年份代码
+ int year = modelYear ?? 2023;
+ char yearCode = GetYearCode(year);
+
+ // 装配厂代码(1位)
+ char plantCode = MathCategory.RandomUtil.GetRandomElement(vdsChars.ToCharArray());
+
+ // 序列号(5位)
+ string sequence = MathCategory.RandomUtil.RandomDigitString(5);
+
+ // 组合16位,计算校验位
+ string vin16 = wmiCode + vds + yearCode + plantCode + sequence;
+ char? checkDigit = CalculateCheckDigit(vin16);
+
+ return vin16.Substring(0, 8) + (checkDigit ?? '0') + vin16.Substring(8);
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 根据年份获取年份代码
+ ///
+ private static char GetYearCode(int year)
+ {
+ foreach (var kvp in YearCodeMap)
+ {
+ if (kvp.Value == year)
+ {
+ return kvp.Key;
+ }
+ }
+ return 'P'; // 默认2023
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/BusinessCategory/WeChatUtil.cs b/EasyTool.Core/BusinessCategory/WeChatUtil.cs
new file mode 100644
index 0000000..6a9b03b
--- /dev/null
+++ b/EasyTool.Core/BusinessCategory/WeChatUtil.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace EasyTool.BusinessCategory
+{
+ ///
+ /// 微信号工具类
+ ///
+ public static class WeChatUtil
+ {
+ #region 常量与私有字段
+
+ ///
+ /// 微信号正则表达式(6-20位,字母开头,字母数字下划线减号)
+ ///
+ private static readonly Regex WeChatIdRegex = new(
+ @"^[a-zA-Z][a-zA-Z0-9_-]{5,19}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 微信原始ID正则表达式(gh_开头)
+ ///
+ private static readonly Regex WeChatOriginalIdRegex = new(
+ @"^gh_[a-zA-Z0-9]{11,12}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 微信开放平台UnionID正则表达式
+ ///
+ private static readonly Regex WeChatUnionIdRegex = new(
+ @"^[a-zA-Z0-9_-]{28,32}$",
+ RegexOptions.Compiled);
+
+ ///
+ /// 微信小程序AppID正则表达式
+ ///
+ private static readonly Regex WeChatAppIdRegex = new(
+ @"^wx[a-f0-9]{16}$",
+ RegexOptions.Compiled);
+
+ #endregion
+
+ #region 验证方法
+
+ ///
+ /// 验证微信号是否有效
+ ///
+ /// 微信号
+ /// 是否有效
+ public static bool IsValid(string? wechatId)
+ {
+ if (string.IsNullOrWhiteSpace(wechatId))
+ {
+ return false;
+ }
+
+ return WeChatIdRegex.IsMatch(wechatId);
+ }
+
+ ///
+ /// 验证微信原始ID是否有效(公众号/小程序)
+ ///
+ /// 原始ID(gh_开头)
+ /// 是否有效
+ public static bool IsValidOriginalId(string? originalId)
+ {
+ if (string.IsNullOrWhiteSpace(originalId))
+ {
+ return false;
+ }
+
+ return WeChatOriginalIdRegex.IsMatch(originalId);
+ }
+
+ ///
+ /// 验证微信UnionID是否有效
+ ///
+ /// UnionID
+ /// 是否有效
+ public static bool IsValidUnionId(string? unionId)
+ {
+ if (string.IsNullOrWhiteSpace(unionId))
+ {
+ return false;
+ }
+
+ return WeChatUnionIdRegex.IsMatch(unionId);
+ }
+
+ ///
+ /// 验证微信小程序AppID是否有效
+ ///
+ /// AppID
+ /// 是否有效
+ public static bool IsValidAppId(string? appId)
+ {
+ if (string.IsNullOrWhiteSpace(appId))
+ {
+ return false;
+ }
+
+ return WeChatAppIdRegex.IsMatch(appId.ToLower());
+ }
+
+ ///
+ /// 验证格式是否正确(仅格式检查)
+ ///
+ /// 微信号
+ /// 格式是否正确
+ public static bool IsValidFormat(string? wechatId)
+ {
+ return IsValid(wechatId);
+ }
+
+ #endregion
+
+ #region 类型识别
+
+ ///
+ /// 获取微信ID类型
+ ///
+ /// 微信相关ID
+ /// ID类型描述
+ public static string? GetIdType(string? id)
+ {
+ if (IsValid(id)) return "微信号";
+ if (IsValidOriginalId(id)) return "微信原始ID";
+ if (IsValidUnionId(id)) return "UnionID";
+ if (IsValidAppId(id)) return "小程序AppID";
+ return null;
+ }
+
+ #endregion
+
+ #region 格式化方法
+
+ ///
+ /// 格式化微信号(转小写)
+ ///
+ /// 微信号
+ /// 格式化后的微信号
+ public static string? Normalize(string? wechatId)
+ {
+ if (string.IsNullOrWhiteSpace(wechatId))
+ {
+ return null;
+ }
+
+ string normalized = wechatId.ToLower().Trim();
+ return IsValid(normalized) ? normalized : null;
+ }
+
+ ///
+ /// 微信号脱敏:abc***xyz
+ ///
+ /// 微信号
+ /// 脱敏后的微信号
+ public static string? Mask(string? wechatId)
+ {
+ if (!IsValid(wechatId))
+ {
+ return null;
+ }
+
+ string id = wechatId!;
+ if (id.Length <= 4)
+ {
+ return id[0] + new string('*', id.Length - 1);
+ }
+
+ // 保留前3位和后3位
+ int prefixLen = 3;
+ int suffixLen = 3;
+ int maskLen = id.Length - prefixLen - suffixLen;
+
+ return id.Substring(0, prefixLen) + new string('*', maskLen) + id.Substring(id.Length - suffixLen);
+ }
+
+ #endregion
+
+ #region 生成方法
+
+ ///
+ /// 生成随机微信号(仅供测试使用)
+ ///
+ /// 随机微信号
+ public static string GenerateRandom()
+ {
+ // 微信号长度6-20位
+ int length = MathCategory.RandomUtil.RandomInt(6, 21);
+
+ // 第一位为字母
+ const string letters = "abcdefghijklmnopqrstuvwxyz";
+ string result = MathCategory.RandomUtil.GetRandomElement(letters.ToCharArray()).ToString();
+
+ // 剩余位为字母数字下划线减号
+ const string chars = "abcdefghijklmnopqrstuvwxyz0123456789_-";
+ for (int i = 1; i < length; i++)
+ {
+ result += MathCategory.RandomUtil.GetRandomElement(chars.ToCharArray());
+ }
+
+ return result;
+ }
+
+ ///
+ /// 生成随机小程序AppID(仅供测试使用)
+ ///
+ /// 随机AppID
+ public static string GenerateRandomAppId()
+ {
+ string hex = "";
+ for (int i = 0; i < 16; i++)
+ {
+ hex += "0123456789abcdef"[MathCategory.RandomUtil.RandomInt(0, 16)];
+ }
+ return "wx" + hex;
+ }
+
+ #endregion
+ }
+}
diff --git a/EasyTool.Core/CacheCategory/CacheOptions.cs b/EasyTool.Core/CacheCategory/CacheOptions.cs
new file mode 100644
index 0000000..eab98c5
--- /dev/null
+++ b/EasyTool.Core/CacheCategory/CacheOptions.cs
@@ -0,0 +1,110 @@
+using System;
+
+namespace EasyTool.CacheCategory
+{
+ ///
+ /// 缓存选项
+ ///
+ public class CacheOptions
+ {
+ ///
+ /// 绝对过期时间
+ ///
+ public DateTime? AbsoluteExpiration { get; set; }
+
+ ///
+ /// 相对过期时间(从现在开始)
+ ///
+ public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
+
+ ///
+ /// 滑动过期时间
+ ///
+ public TimeSpan? SlidingExpiration { get; set; }
+
+ ///
+ /// 缓存优先级
+ ///
+ public CachePriority Priority { get; set; } = CachePriority.Normal;
+
+ ///
+ /// 缓存键前缀
+ ///
+ public string? KeyPrefix { get; set; }
+
+ ///
+ /// 是否启用压缩
+ ///
+ public bool EnableCompression { get; set; }
+
+ ///
+ /// 压缩阈值(字节)
+ ///
+ public int CompressionThreshold { get; set; } = 1024;
+
+ ///
+ /// 创建相对过期选项
+ ///
+ /// 过期时间
+ /// 缓存选项
+ public static CacheOptions FromExpiration(TimeSpan expiration)
+ {
+ return new CacheOptions
+ {
+ AbsoluteExpirationRelativeToNow = expiration
+ };
+ }
+
+ ///
+ /// 创建滑动过期选项
+ ///
+ /// 滑动过期时间
+ /// 缓存选项
+ public static CacheOptions FromSlidingExpiration(TimeSpan slidingExpiration)
+ {
+ return new CacheOptions
+ {
+ SlidingExpiration = slidingExpiration
+ };
+ }
+
+ ///
+ /// 创建绝对过期选项
+ ///
+ /// 绝对过期时间
+ /// 缓存选项
+ public static CacheOptions FromAbsoluteExpiration(DateTime absoluteExpiration)
+ {
+ return new CacheOptions
+ {
+ AbsoluteExpiration = absoluteExpiration
+ };
+ }
+ }
+
+ ///
+ /// 缓存优先级
+ ///
+ public enum CachePriority
+ {
+ ///
+ /// 低优先级
+ ///
+ Low = 0,
+
+ ///
+ /// 普通优先级
+ ///
+ Normal = 1,
+
+ ///
+ /// 高优先级
+ ///
+ High = 2,
+
+ ///
+ /// 永不移除
+ ///
+ NeverRemove = 3
+ }
+}
diff --git a/EasyTool.Core/CacheCategory/DistributedCacheUtil.cs b/EasyTool.Core/CacheCategory/DistributedCacheUtil.cs
new file mode 100644
index 0000000..3360974
--- /dev/null
+++ b/EasyTool.Core/CacheCategory/DistributedCacheUtil.cs
@@ -0,0 +1,500 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EasyTool.CacheCategory
+{
+ ///
+ /// 分布式缓存工具类
+ /// 提供多级缓存支持,包括本地缓存和分布式缓存
+ ///
+ public static class DistributedCacheUtil
+ {
+ private static readonly ConcurrentDictionary _providers = new();
+ private static ICacheProvider? _defaultProvider;
+ private static readonly object _lock = new();
+
+ ///
+ /// 注册缓存提供者
+ ///
+ /// 提供者名称
+ /// 缓存提供者
+ /// 是否设为默认
+ public static void RegisterProvider(string name, ICacheProvider provider, bool setDefault = false)
+ {
+ _providers[name] = provider;
+
+ if (setDefault || _defaultProvider == null)
+ {
+ _defaultProvider = provider;
+ }
+ }
+
+ ///
+ /// 获取缓存提供者
+ ///
+ /// 提供者名称
+ /// 缓存提供者
+ public static ICacheProvider? GetProvider(string name)
+ {
+ return _providers.TryGetValue(name, out var provider) ? provider : null;
+ }
+
+ ///
+ /// 获取默认缓存提供者
+ ///
+ public static ICacheProvider DefaultProvider
+ {
+ get
+ {
+ if (_defaultProvider == null)
+ {
+ lock (_lock)
+ {
+ if (_defaultProvider == null)
+ {
+ _defaultProvider = new MemoryCacheProvider();
+ _providers["default"] = _defaultProvider;
+ }
+ }
+ }
+ return _defaultProvider;
+ }
+ }
+
+ ///
+ /// 创建内存缓存提供者
+ ///
+ /// 清理间隔
+ /// 大小限制
+ /// 缓存提供者
+ public static MemoryCacheProvider CreateMemoryProvider(TimeSpan? cleanupInterval = null, long? sizeLimit = null)
+ {
+ return new MemoryCacheProvider(cleanupInterval, sizeLimit);
+ }
+
+ ///
+ /// 创建 Redis 缓存提供者
+ ///
+ /// Redis 配置
+ /// 缓存提供者
+ public static RedisCacheProvider CreateRedisProvider(RedisCacheOptions? options = null)
+ {
+ return new RedisCacheProvider(options);
+ }
+
+ #region 便捷方法 - 使用默认提供者
+
+ ///
+ /// 设置缓存
+ ///
+ public static Task SetAsync(string key, T value, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return DefaultProvider.SetAsync(key, value, options, cancellationToken);
+ }
+
+ ///
+ /// 设置缓存(同步)
+ ///
+ public static void Set(string key, T value, CacheOptions? options = null)
+ {
+ DefaultProvider.Set(key, value, options);
+ }
+
+ ///
+ /// 获取缓存
+ ///
+ public static Task GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ return DefaultProvider.GetAsync(key, cancellationToken);
+ }
+
+ ///
+ /// 获取缓存(同步)
+ ///
+ public static T? Get(string key)
+ {
+ return DefaultProvider.Get(key);
+ }
+
+ ///
+ /// 获取或添加缓存
+ ///
+ public static Task GetOrAddAsync(string key, Func> factory, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return DefaultProvider.GetOrAddAsync(key, factory, options, cancellationToken);
+ }
+
+ ///
+ /// 获取或添加缓存(同步)
+ ///
+ public static T GetOrAdd(string key, Func factory, CacheOptions? options = null)
+ {
+ return DefaultProvider.GetOrAdd(key, factory, options);
+ }
+
+ ///
+ /// 检查缓存是否存在
+ ///
+ public static Task ExistsAsync(string key, CancellationToken cancellationToken = default)
+ {
+ return DefaultProvider.ExistsAsync(key, cancellationToken);
+ }
+
+ ///
+ /// 检查缓存是否存在(同步)
+ ///
+ public static bool Exists(string key)
+ {
+ return DefaultProvider.Exists(key);
+ }
+
+ ///
+ /// 移除缓存
+ ///
+ public static Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ return DefaultProvider.RemoveAsync(key, cancellationToken);
+ }
+
+ ///
+ /// 移除缓存(同步)
+ ///
+ public static void Remove(string key)
+ {
+ DefaultProvider.Remove(key);
+ }
+
+ ///
+ /// 清空缓存
+ ///
+ public static Task ClearAsync(CancellationToken cancellationToken = default)
+ {
+ return DefaultProvider.ClearAsync(cancellationToken);
+ }
+
+ ///
+ /// 清空缓存(同步)
+ ///
+ public static void Clear()
+ {
+ DefaultProvider.Clear();
+ }
+
+ #endregion
+
+ #region 高级功能
+
+ ///
+ /// 批量获取缓存
+ ///
+ /// 值类型
+ /// 缓存键集合
+ /// 取消令牌
+ /// 键值对字典
+ public static async Task> GetManyAsync(IEnumerable keys, CancellationToken cancellationToken = default)
+ {
+ var result = new Dictionary();
+
+ foreach (var key in keys)
+ {
+ var value = await DefaultProvider.GetAsync(key, cancellationToken);
+ result[key] = value;
+ }
+
+ return result;
+ }
+
+ ///
+ /// 批量设置缓存
+ ///
+ /// 值类型
+ /// 键值对集合
+ /// 缓存选项
+ /// 取消令牌
+ public static async Task SetManyAsync(IDictionary items, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ foreach (var item in items)
+ {
+ await DefaultProvider.SetAsync(item.Key, item.Value, options, cancellationToken);
+ }
+ }
+
+ ///
+ /// 获取或添加缓存(带锁)
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 值工厂
+ /// 缓存选项
+ /// 锁超时时间
+ /// 取消令牌
+ /// 缓存值
+ public static async Task GetOrAddWithLockAsync(
+ string key,
+ Func> factory,
+ CacheOptions? options = null,
+ TimeSpan? lockTimeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ var value = await DefaultProvider.GetAsync(key, cancellationToken);
+ if (value != null || typeof(T).IsValueType)
+ {
+ if (value != null)
+ return value;
+ }
+
+ // 使用简单的锁机制防止缓存穿透
+ var lockKey = $"lock:{key}";
+ var timeout = lockTimeout ?? TimeSpan.FromSeconds(30);
+ var startTime = DateTime.UtcNow;
+
+ while (DateTime.UtcNow - startTime < timeout)
+ {
+ value = await DefaultProvider.GetAsync(key, cancellationToken);
+ if (value != null || (typeof(T).IsValueType && value != null))
+ return value!;
+
+ value = await factory();
+ await DefaultProvider.SetAsync(key, value, options, cancellationToken);
+ return value;
+ }
+
+ throw new TimeoutException($"获取缓存超时: {key}");
+ }
+
+ ///
+ /// 刷新缓存(强制重新加载)
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 值工厂
+ /// 缓存选项
+ /// 取消令牌
+ /// 新的缓存值
+ public static async Task RefreshAsync(string key, Func> factory, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ await DefaultProvider.RemoveAsync(key, cancellationToken);
+ var value = await factory();
+ await DefaultProvider.SetAsync(key, value, options, cancellationToken);
+ return value;
+ }
+
+ #endregion
+ }
+
+ ///
+ /// 多级缓存
+ /// 实现本地缓存 + 分布式缓存的多级缓存策略
+ ///
+ public class MultiLevelCache : ICacheProvider, IDisposable
+ {
+ private readonly MemoryCacheProvider _localCache;
+ private readonly ICacheProvider? _distributedCache;
+ private readonly TimeSpan _localCacheExpiration;
+
+ ///
+ /// 创建多级缓存
+ ///
+ /// 分布式缓存提供者
+ /// 本地缓存过期时间
+ public MultiLevelCache(ICacheProvider? distributedCache = null, TimeSpan? localCacheExpiration = null)
+ {
+ _localCache = new MemoryCacheProvider();
+ _distributedCache = distributedCache;
+ _localCacheExpiration = localCacheExpiration ?? TimeSpan.FromMinutes(5);
+ }
+
+ ///
+ public async Task SetAsync(string key, T value, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ // 先设置本地缓存
+ var localOptions = new CacheOptions
+ {
+ AbsoluteExpirationRelativeToNow = _localCacheExpiration
+ };
+ await _localCache.SetAsync(key, value, localOptions, cancellationToken);
+
+ // 再设置分布式缓存
+ if (_distributedCache != null)
+ {
+ await _distributedCache.SetAsync(key, value, options, cancellationToken);
+ }
+ }
+
+ ///
+ public void Set(string key, T value, CacheOptions? options = null)
+ {
+ SetAsync(key, value, options).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ // 先查本地缓存
+ var value = await _localCache.GetAsync(key, cancellationToken);
+ if (value != null || typeof(T).IsValueType)
+ {
+ if (value != null)
+ return value;
+ }
+
+ // 再查分布式缓存
+ if (_distributedCache != null)
+ {
+ value = await _distributedCache.GetAsync(key, cancellationToken);
+ if (value != null)
+ {
+ // 回填本地缓存
+ await _localCache.SetAsync(key, value, new CacheOptions
+ {
+ AbsoluteExpirationRelativeToNow = _localCacheExpiration
+ }, cancellationToken);
+ }
+ }
+
+ return value;
+ }
+
+ ///
+ public T? Get(string key)
+ {
+ return GetAsync(key).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task GetOrAddAsync(string key, Func> factory, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var value = await GetAsync(key, cancellationToken);
+ if (value != null || typeof(T).IsValueType)
+ {
+ if (value != null)
+ return value;
+ }
+
+ value = await factory();
+ await SetAsync(key, value, options, cancellationToken);
+ return value;
+ }
+
+ ///
+ public T GetOrAdd(string key, Func factory, CacheOptions? options = null)
+ {
+ return GetOrAddAsync(key, () => Task.FromResult(factory()), options).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task ExistsAsync(string key, CancellationToken cancellationToken = default)
+ {
+ if (await _localCache.ExistsAsync(key, cancellationToken))
+ return true;
+
+ return _distributedCache != null && await _distributedCache.ExistsAsync(key, cancellationToken);
+ }
+
+ ///
+ public bool Exists(string key)
+ {
+ return ExistsAsync(key).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ await _localCache.RemoveAsync(key, cancellationToken);
+
+ if (_distributedCache != null)
+ {
+ await _distributedCache.RemoveAsync(key, cancellationToken);
+ }
+ }
+
+ ///
+ public void Remove(string key)
+ {
+ RemoveAsync(key).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task RemoveAsync(IEnumerable keys, CancellationToken cancellationToken = default)
+ {
+ await _localCache.RemoveAsync(keys, cancellationToken);
+
+ if (_distributedCache != null)
+ {
+ await _distributedCache.RemoveAsync(keys, cancellationToken);
+ }
+ }
+
+ ///
+ public void Remove(IEnumerable keys)
+ {
+ RemoveAsync(keys).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task SetExpirationAsync(string key, TimeSpan expiration, CancellationToken cancellationToken = default)
+ {
+ var localResult = await _localCache.SetExpirationAsync(key, expiration, cancellationToken);
+
+ if (_distributedCache != null)
+ {
+ return await _distributedCache.SetExpirationAsync(key, expiration, cancellationToken);
+ }
+
+ return localResult;
+ }
+
+ ///
+ public bool SetExpiration(string key, TimeSpan expiration)
+ {
+ return SetExpirationAsync(key, expiration).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task ClearAsync(CancellationToken cancellationToken = default)
+ {
+ await _localCache.ClearAsync(cancellationToken);
+
+ if (_distributedCache != null)
+ {
+ await _distributedCache.ClearAsync(cancellationToken);
+ }
+ }
+
+ ///
+ public void Clear()
+ {
+ ClearAsync().GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task CountAsync(CancellationToken cancellationToken = default)
+ {
+ var count = await _localCache.CountAsync(cancellationToken);
+
+ if (_distributedCache != null)
+ {
+ count = await _distributedCache.CountAsync(cancellationToken);
+ }
+
+ return count;
+ }
+
+ ///
+ public long Count()
+ {
+ return CountAsync().GetAwaiter().GetResult();
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ _localCache.Dispose();
+ }
+ }
+}
diff --git a/EasyTool.Core/CacheCategory/ICacheProvider.cs b/EasyTool.Core/CacheCategory/ICacheProvider.cs
new file mode 100644
index 0000000..7d5e7d4
--- /dev/null
+++ b/EasyTool.Core/CacheCategory/ICacheProvider.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EasyTool.CacheCategory
+{
+ ///
+ /// 缓存提供者接口
+ ///
+ public interface ICacheProvider
+ {
+ ///
+ /// 设置缓存
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 缓存值
+ /// 缓存选项
+ /// 取消令牌
+ Task SetAsync(string key, T value, CacheOptions? options = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// 设置缓存(同步)
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 缓存值
+ /// 缓存选项
+ void Set(string key, T value, CacheOptions? options = null);
+
+ ///
+ /// 获取缓存
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 取消令牌
+ /// 缓存值
+ Task GetAsync(string key, CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取缓存(同步)
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 缓存值
+ T? Get(string key);
+
+ ///
+ /// 获取或添加缓存
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 值工厂
+ /// 缓存选项
+ /// 取消令牌
+ /// 缓存值
+ Task GetOrAddAsync(string key, Func> factory, CacheOptions? options = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取或添加缓存(同步)
+ ///
+ /// 值类型
+ /// 缓存键
+ /// 值工厂
+ /// 缓存选项
+ /// 缓存值
+ T GetOrAdd(string key, Func factory, CacheOptions? options = null);
+
+ ///
+ /// 检查缓存是否存在
+ ///
+ /// 缓存键
+ /// 取消令牌
+ /// 是否存在
+ Task ExistsAsync(string key, CancellationToken cancellationToken = default);
+
+ ///
+ /// 检查缓存是否存在(同步)
+ ///
+ /// 缓存键
+ /// 是否存在
+ bool Exists(string key);
+
+ ///
+ /// 移除缓存
+ ///
+ /// 缓存键
+ /// 取消令牌
+ Task RemoveAsync(string key, CancellationToken cancellationToken = default);
+
+ ///
+ /// 移除缓存(同步)
+ ///
+ /// 缓存键
+ void Remove(string key);
+
+ ///
+ /// 批量移除缓存
+ ///
+ /// 缓存键集合
+ /// 取消令牌
+ Task RemoveAsync(IEnumerable keys, CancellationToken cancellationToken = default);
+
+ ///
+ /// 批量移除缓存(同步)
+ ///
+ /// 缓存键集合
+ void Remove(IEnumerable keys);
+
+ ///
+ /// 设置过期时间
+ ///
+ /// 缓存键
+ /// 过期时间
+ /// 取消令牌
+ /// 是否设置成功
+ Task SetExpirationAsync(string key, TimeSpan expiration, CancellationToken cancellationToken = default);
+
+ ///
+ /// 设置过期时间(同步)
+ ///
+ /// 缓存键
+ /// 过期时间
+ /// 是否设置成功
+ bool SetExpiration(string key, TimeSpan expiration);
+
+ ///
+ /// 清空所有缓存
+ ///
+ /// 取消令牌
+ Task ClearAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// 清空所有缓存(同步)
+ ///
+ void Clear();
+
+ ///
+ /// 获取缓存数量
+ ///
+ /// 取消令牌
+ /// 缓存项数量
+ Task CountAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取缓存数量(同步)
+ ///
+ /// 缓存项数量
+ long Count();
+ }
+}
diff --git a/EasyTool.Core/CacheCategory/MemoryCacheProvider.cs b/EasyTool.Core/CacheCategory/MemoryCacheProvider.cs
new file mode 100644
index 0000000..62d122e
--- /dev/null
+++ b/EasyTool.Core/CacheCategory/MemoryCacheProvider.cs
@@ -0,0 +1,399 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EasyTool.CacheCategory
+{
+ ///
+ /// 内存缓存项
+ ///
+ internal class MemoryCacheItem
+ {
+ public object? Value { get; set; }
+ public DateTime CreateTime { get; set; }
+ public DateTime? AbsoluteExpiration { get; set; }
+ public TimeSpan? SlidingExpiration { get; set; }
+ public DateTime LastAccess { get; set; }
+ public CachePriority Priority { get; set; }
+ public Type ValueType { get; set; } = typeof(object);
+ }
+
+ ///
+ /// 内存缓存提供者
+ /// 提供高性能的内存缓存实现,支持过期策略和优先级
+ ///
+ public class MemoryCacheProvider : ICacheProvider, IDisposable
+ {
+ private readonly ConcurrentDictionary _cache;
+ private readonly Timer? _cleanupTimer;
+ private readonly long? _sizeLimit;
+ private long _currentSize;
+ private bool _disposed;
+
+ ///
+ /// 创建内存缓存提供者
+ ///
+ /// 清理间隔
+ /// 大小限制(项数)
+ public MemoryCacheProvider(TimeSpan? cleanupInterval = null, long? sizeLimit = null)
+ {
+ _cache = new ConcurrentDictionary();
+ _sizeLimit = sizeLimit;
+ _currentSize = 0;
+
+ // 定期清理过期缓存
+ var interval = cleanupInterval ?? TimeSpan.FromMinutes(1);
+ _cleanupTimer = new Timer(CleanupExpired, null, interval, interval);
+ }
+
+ ///
+ public Task SetAsync(string key, T value, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Set(key, value, options);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void Set(string key, T value, CacheOptions? options = null)
+ {
+ if (string.IsNullOrEmpty(key))
+ throw new ArgumentNullException(nameof(key));
+
+ options ??= new CacheOptions();
+
+ var item = new MemoryCacheItem
+ {
+ Value = value,
+ ValueType = typeof(T),
+ CreateTime = DateTime.UtcNow,
+ LastAccess = DateTime.UtcNow,
+ Priority = options.Priority,
+ SlidingExpiration = options.SlidingExpiration
+ };
+
+ // 计算过期时间
+ if (options.AbsoluteExpiration.HasValue)
+ {
+ item.AbsoluteExpiration = options.AbsoluteExpiration.Value.ToUniversalTime();
+ }
+ else if (options.AbsoluteExpirationRelativeToNow.HasValue)
+ {
+ item.AbsoluteExpiration = DateTime.UtcNow.Add(options.AbsoluteExpirationRelativeToNow.Value);
+ }
+
+ // 添加键前缀
+ var cacheKey = options.KeyPrefix != null
+ ? $"{options.KeyPrefix}:{key}"
+ : key;
+
+ _cache.AddOrUpdate(cacheKey, item, (k, old) =>
+ {
+ Interlocked.Decrement(ref _currentSize);
+ return item;
+ });
+
+ Interlocked.Increment(ref _currentSize);
+
+ // 检查容量限制
+ if (_sizeLimit.HasValue && _currentSize > _sizeLimit.Value)
+ {
+ EvictLowPriorityItems();
+ }
+ }
+
+ ///
+ public Task GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(Get(key));
+ }
+
+ ///
+ public T? Get(string key)
+ {
+ if (string.IsNullOrEmpty(key))
+ return default;
+
+ if (!_cache.TryGetValue(key, out var item))
+ return default;
+
+ if (IsExpired(item))
+ {
+ _cache.TryRemove(key, out _);
+ Interlocked.Decrement(ref _currentSize);
+ return default;
+ }
+
+ // 更新滑动过期
+ if (item.SlidingExpiration.HasValue)
+ {
+ item.LastAccess = DateTime.UtcNow;
+ }
+
+ return (T?)item.Value;
+ }
+
+ ///
+ public async Task GetOrAddAsync(string key, Func> factory, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var value = Get(key);
+ if (value != null || typeof(T).IsValueType)
+ {
+ if (value != null)
+ return value;
+ }
+
+ value = await factory();
+ Set(key, value, options);
+ return value;
+ }
+
+ ///
+ public T GetOrAdd(string key, Func factory, CacheOptions? options = null)
+ {
+ var value = Get(key);
+ if (value != null || typeof(T).IsValueType)
+ {
+ if (value != null)
+ return value;
+ }
+
+ value = factory();
+ Set(key, value, options);
+ return value;
+ }
+
+ ///
+ public Task ExistsAsync(string key, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(Exists(key));
+ }
+
+ ///
+ public bool Exists(string key)
+ {
+ if (string.IsNullOrEmpty(key))
+ return false;
+
+ if (!_cache.TryGetValue(key, out var item))
+ return false;
+
+ if (IsExpired(item))
+ {
+ _cache.TryRemove(key, out _);
+ Interlocked.Decrement(ref _currentSize);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ Remove(key);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void Remove(string key)
+ {
+ if (_cache.TryRemove(key, out _))
+ {
+ Interlocked.Decrement(ref _currentSize);
+ }
+ }
+
+ ///
+ public Task RemoveAsync(IEnumerable keys, CancellationToken cancellationToken = default)
+ {
+ Remove(keys);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void Remove(IEnumerable keys)
+ {
+ foreach (var key in keys)
+ {
+ Remove(key);
+ }
+ }
+
+ ///
+ public Task SetExpirationAsync(string key, TimeSpan expiration, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(SetExpiration(key, expiration));
+ }
+
+ ///
+ public bool SetExpiration(string key, TimeSpan expiration)
+ {
+ if (!_cache.TryGetValue(key, out var item))
+ return false;
+
+ item.AbsoluteExpiration = DateTime.UtcNow.Add(expiration);
+ return true;
+ }
+
+ ///
+ public Task ClearAsync(CancellationToken cancellationToken = default)
+ {
+ Clear();
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void Clear()
+ {
+ _cache.Clear();
+ Interlocked.Exchange(ref _currentSize, 0);
+ }
+
+ ///
+ public Task CountAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(Count());
+ }
+
+ ///
+ public long Count()
+ {
+ return _cache.Count;
+ }
+
+ ///
+ /// 获取所有缓存键
+ ///
+ /// 缓存键集合
+ public IEnumerable GetKeys()
+ {
+ return _cache.Keys.ToList();
+ }
+
+ ///
+ /// 获取缓存统计信息
+ ///
+ /// 统计信息
+ public CacheStatistics GetStatistics()
+ {
+ var now = DateTime.UtcNow;
+ var items = _cache.Values.ToList();
+
+ return new CacheStatistics
+ {
+ TotalCount = items.Count,
+ ExpiredCount = items.Count(i => IsExpired(i)),
+ HighPriorityCount = items.Count(i => i.Priority == CachePriority.High),
+ LowPriorityCount = items.Count(i => i.Priority == CachePriority.Low),
+ EstimatedSize = _currentSize
+ };
+ }
+
+ private bool IsExpired(MemoryCacheItem item)
+ {
+ var now = DateTime.UtcNow;
+
+ // 检查绝对过期
+ if (item.AbsoluteExpiration.HasValue && now >= item.AbsoluteExpiration.Value)
+ return true;
+
+ // 检查滑动过期
+ if (item.SlidingExpiration.HasValue)
+ {
+ var expireTime = item.LastAccess.Add(item.SlidingExpiration.Value);
+ if (now >= expireTime)
+ return true;
+ }
+
+ return false;
+ }
+
+ private void CleanupExpired(object? state)
+ {
+ var keysToRemove = new List();
+
+ foreach (var kvp in _cache)
+ {
+ if (IsExpired(kvp.Value))
+ {
+ keysToRemove.Add(kvp.Key);
+ }
+ }
+
+ foreach (var key in keysToRemove)
+ {
+ if (_cache.TryRemove(key, out _))
+ {
+ Interlocked.Decrement(ref _currentSize);
+ }
+ }
+ }
+
+ private void EvictLowPriorityItems()
+ {
+ // 按优先级和访问时间排序,移除低优先级的项
+ var itemsToEvict = _cache
+ .Where(kvp => kvp.Value.Priority != CachePriority.NeverRemove)
+ .OrderBy(kvp => (int)kvp.Value.Priority)
+ .ThenBy(kvp => kvp.Value.LastAccess)
+ .Take((int)(_currentSize - _sizeLimit!.Value + 10))
+ .Select(kvp => kvp.Key)
+ .ToList();
+
+ foreach (var key in itemsToEvict)
+ {
+ if (_cache.TryRemove(key, out _))
+ {
+ Interlocked.Decrement(ref _currentSize);
+ }
+ }
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _cleanupTimer?.Dispose();
+ _cache.Clear();
+ _disposed = true;
+ }
+ }
+ }
+
+ ///
+ /// 缓存统计信息
+ ///
+ public class CacheStatistics
+ {
+ ///
+ /// 总缓存项数
+ ///
+ public long TotalCount { get; set; }
+
+ ///
+ /// 已过期项数
+ ///
+ public long ExpiredCount { get; set; }
+
+ ///
+ /// 高优先级项数
+ ///
+ public long HighPriorityCount { get; set; }
+
+ ///
+ /// 低优先级项数
+ ///
+ public long LowPriorityCount { get; set; }
+
+ ///
+ /// 估计大小
+ ///
+ public long EstimatedSize { get; set; }
+ }
+}
diff --git a/EasyTool.Core/CacheCategory/RedisCacheProvider.cs b/EasyTool.Core/CacheCategory/RedisCacheProvider.cs
new file mode 100644
index 0000000..9148bce
--- /dev/null
+++ b/EasyTool.Core/CacheCategory/RedisCacheProvider.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EasyTool.CacheCategory
+{
+ ///
+ /// Redis 缓存配置
+ ///
+ public class RedisCacheOptions
+ {
+ ///
+ /// Redis 连接字符串
+ ///
+ public string ConnectionString { get; set; } = "localhost:6379";
+
+ ///
+ /// 实例名称
+ ///
+ public string InstanceName { get; set; } = "";
+
+ ///
+ /// 默认数据库
+ ///
+ public int DefaultDatabase { get; set; } = 0;
+
+ ///
+ /// 默认过期时间
+ ///
+ public TimeSpan? DefaultExpiration { get; set; }
+
+ ///
+ /// 连接超时
+ ///
+ public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(5);
+
+ ///
+ /// 是否允许管理员操作
+ ///
+ public bool AllowAdmin { get; set; }
+
+ ///
+ /// 是否使用 SSL
+ ///
+ public bool UseSsl { get; set; }
+
+ ///
+ /// 密码
+ ///
+ public string? Password { get; set; }
+ }
+
+ ///
+ /// Redis 缓存提供者
+ /// 注意:此类提供 Redis 缓存的抽象接口,实际使用需要引入 StackExchange.Redis 包
+ ///
+ public class RedisCacheProvider : ICacheProvider, IAsyncDisposable, IDisposable
+ {
+ private readonly RedisCacheOptions _options;
+ private readonly string _keyPrefix;
+#pragma warning disable CS0169, CS0649 // 字段保留供扩展使用
+ private object? _connectionMultiplexer;
+#pragma warning restore CS0169, CS0649
+#pragma warning disable CS0169 // 字段保留供扩展使用
+ private object? _database;
+#pragma warning restore CS0169
+ private bool _disposed;
+
+ ///
+ /// 创建 Redis 缓存提供者
+ ///
+ /// Redis 配置
+ public RedisCacheProvider(RedisCacheOptions? options = null)
+ {
+ _options = options ?? new RedisCacheOptions();
+ _keyPrefix = string.IsNullOrEmpty(_options.InstanceName)
+ ? ""
+ : _options.InstanceName + ":";
+ }
+
+ ///
+ /// 获取 Redis 连接(需要 StackExchange.Redis)
+ /// 此方法为扩展点,子类可重写以实现具体的 Redis 连接逻辑
+ ///
+ protected virtual object? GetConnection()
+ {
+ // 这是一个占位实现
+ // 实际使用时需要引入 StackExchange.Redis 并实现连接逻辑
+ throw new NotImplementedException(
+ "请引入 StackExchange.Redis 包并实现 Redis 连接逻辑," +
+ "或使用 DistributedCacheUtil.CreateRedisProvider 方法");
+ }
+
+ private string GetFullKey(string key) => $"{_keyPrefix}{key}";
+
+ ///
+ public async Task SetAsync(string key, T value, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+
+ var fullKey = GetFullKey(key);
+ var expiration = GetExpiration(options);
+
+ // 这里需要实际的 Redis 实现来设置值
+ await Task.CompletedTask;
+ }
+
+ ///
+ public void Set(string key, T value, CacheOptions? options = null)
+ {
+ SetAsync(key, value, options).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+ await Task.CompletedTask;
+ return default;
+ }
+
+ ///
+ public T? Get(string key)
+ {
+ return GetAsync(key).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task GetOrAddAsync(string key, Func> factory, CacheOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var value = await GetAsync(key, cancellationToken);
+ if (value != null || typeof(T).IsValueType)
+ {
+ if (value != null)
+ return value;
+ }
+
+ value = await factory();
+ await SetAsync(key, value, options, cancellationToken);
+ return value;
+ }
+
+ ///
+ public T GetOrAdd(string key, Func factory, CacheOptions? options = null)
+ {
+ return GetOrAddAsync(key, () => Task.FromResult(factory()), options).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task ExistsAsync(string key, CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+ await Task.CompletedTask;
+ return false;
+ }
+
+ ///
+ public bool Exists(string key)
+ {
+ return ExistsAsync(key).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+ await Task.CompletedTask;
+ }
+
+ ///
+ public void Remove(string key)
+ {
+ RemoveAsync(key).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task RemoveAsync(IEnumerable keys, CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+ await Task.CompletedTask;
+ }
+
+ ///
+ public void Remove(IEnumerable keys)
+ {
+ RemoveAsync(keys).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task SetExpirationAsync(string key, TimeSpan expiration, CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+ await Task.CompletedTask;
+ return false;
+ }
+
+ ///
+ public bool SetExpiration(string key, TimeSpan expiration)
+ {
+ return SetExpirationAsync(key, expiration).GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task ClearAsync(CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+ await Task.CompletedTask;
+ }
+
+ ///
+ public void Clear()
+ {
+ ClearAsync().GetAwaiter().GetResult();
+ }
+
+ ///
+ public async Task CountAsync(CancellationToken cancellationToken = default)
+ {
+ ThrowIfNotImplemented();
+ await Task.CompletedTask;
+ return 0;
+ }
+
+ ///
+ public long Count()
+ {
+ return CountAsync().GetAwaiter().GetResult();
+ }
+
+ private TimeSpan? GetExpiration(CacheOptions? options)
+ {
+ if (options?.AbsoluteExpirationRelativeToNow != null)
+ return options.AbsoluteExpirationRelativeToNow;
+
+ if (options?.AbsoluteExpiration != null)
+ return options.AbsoluteExpiration.Value - DateTime.UtcNow;
+
+ if (options?.SlidingExpiration != null)
+ return options.SlidingExpiration;
+
+ return _options.DefaultExpiration;
+ }
+
+ private void ThrowIfNotImplemented()
+ {
+ if (_connectionMultiplexer == null)
+ {
+ throw new NotImplementedException(
+ "Redis 缓存提供者需要实际实现。请引入 StackExchange.Redis 包," +
+ "或使用 MemoryCacheProvider 作为替代。");
+ }
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ (_connectionMultiplexer as IDisposable)?.Dispose();
+ _disposed = true;
+ }
+ }
+
+ ///
+ /// 异步释放资源
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (!_disposed)
+ {
+ if (_connectionMultiplexer is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.DisposeAsync();
+ }
+ else if (_connectionMultiplexer is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/EasyTool.Core/CloneCategory/CloneExtension.cs b/EasyTool.Core/CloneCategory/CloneExtension.cs
deleted file mode 100644
index 43f892d..0000000
--- a/EasyTool.Core/CloneCategory/CloneExtension.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace EasyTool.Extension
-{
- public static class CloneExtension
- {
- //定义一个泛型方法,接受一个泛型参数 T,并返回一个 T 类型的对象
- public static T Clone(this T obj)=> CloneUtil.Clone(obj);
- }
-}
diff --git a/EasyTool.Core/CloneCategory/CloneUtil.cs b/EasyTool.Core/CloneCategory/CloneUtil.cs
deleted file mode 100644
index 646e98e..0000000
--- a/EasyTool.Core/CloneCategory/CloneUtil.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System;
-using System.IO;
-using System.Runtime.Serialization.Formatters.Binary;
-using System.Runtime.Serialization;
-using System.Threading.Tasks;
-
-namespace EasyTool
-{
- ///