Compare commits
8 Commits
eb41f35a03
...
8a02234dcc
| Author | SHA1 | Date |
|---|---|---|
|
|
8a02234dcc | |
|
|
23153a8672 | |
|
|
09fce0754d | |
|
|
509d57596f | |
|
|
147194a116 | |
|
|
03b50542fa | |
|
|
48cab32859 | |
|
|
46b760cef6 |
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(./mvnw --version)",
|
||||||
|
"Bash(./mvnw help:active-profiles)",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/command/AddGoodsCommand.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/command/UpdateGoodsCommand.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/db/ShopGoodsEntity.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/db/ShopGoodsMapper.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/db/ShopGoodsService.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/db/ShopGoodsServiceImpl.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/dto/ShopGoodsDTO.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/model/GoodsModel.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/model/GoodsModelFactory.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/query/SearchShopGoodsQuery.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/GoodsApplicationService.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/cabinet/cell/command/AddCabinetCellCommand.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/cabinet/cell/db/CabinetCellEntity.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/cabinet/cell/model/CabinetCellModel.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/command/AddAb98UserCommand.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/db/Ab98UserEntity.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/ab98/user/model/Ab98UserModel.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/command/AddUserCommand.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/db/SysUserEntity.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/model/UserModel.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/common/core/base/BaseEntity.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/common/command/BulkOperationCommand.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/common/core/page/AbstractPageQuery.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-common/src/main/java/com/agileboot/common/core/base/BaseEntity.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-common/src/main/java/com/agileboot/common/core/page/AbstractPageQuery.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/UserApplicationService.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/db/SysUserServiceImpl.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/model/UserModelFactory.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/dto/UserDTO.java\")",
|
||||||
|
"Bash(cat \"/e/code/智柜宝/shop-back-end/agileboot-domain/src/main/java/com/agileboot/domain/system/user/query/SearchUserQuery.java\")",
|
||||||
|
"Bash(./mvnw clean compile -pl agileboot-api -am -DskipTests)",
|
||||||
|
"Bash(./mvnw clean compile -pl agileboot-api,agileboot-domain -am -DskipTests)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable"
|
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable",
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic"
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is **AgileBoot** - a Spring Boot 2.7.10 + Vue3 full-stack development framework with **DDD/CQRS architecture**. The project specializes in e-commerce business scenarios and integrates Enterprise WeChat, WeChat Pay, and Smart Cabinet systems.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- DDD (Domain-Driven Design) with CQRS pattern
|
||||||
|
- Multi-level caching (Caffeine + Guava + Redis)
|
||||||
|
- Enterprise WeChat integration (supports corpid)
|
||||||
|
- Smart cabinet device management
|
||||||
|
- WeChat Pay integration (JSAPI)
|
||||||
|
- JWT-based authentication with Spring Security
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Backend**: Spring Boot 2.7.10, Java 8
|
||||||
|
- **Database**: MySQL 8.0, MyBatis Plus 3.5.2
|
||||||
|
- **Cache**: Caffeine (local), Redis (distributed), Guava (config)
|
||||||
|
- **Security**: Spring Security + JWT
|
||||||
|
- **Documentation**: SpringDoc OpenAPI 3.0
|
||||||
|
- **Build Tool**: Maven 3.9+ (use `./mvnw` wrapper)
|
||||||
|
- **Testing**: JUnit, Mockito
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
agileboot (multi-module Maven project)
|
||||||
|
├── agileboot-admin # Management backend interface module
|
||||||
|
├── agileboot-api # Open API module (for clients)
|
||||||
|
├── agileboot-common # Common utilities module
|
||||||
|
├── agileboot-domain # Business domain module (DDD)
|
||||||
|
├── agileboot-infrastructure # Infrastructure module (config, integration)
|
||||||
|
└── agileboot-orm # ORM configuration (legacy)
|
||||||
|
|
||||||
|
agileboot-domain (DDD structure - CQRS pattern)
|
||||||
|
├── {business-module}/
|
||||||
|
│ ├── command/ # Command objects (data update)
|
||||||
|
│ ├── dto/ # Data Transfer Objects
|
||||||
|
│ ├── query/ # Query objects (data retrieval)
|
||||||
|
│ ├── model/ # Domain models
|
||||||
|
│ ├── db/ # Database layer
|
||||||
|
│ │ ├── entity/ # Entity classes
|
||||||
|
│ │ ├── service/ # Database service
|
||||||
|
│ │ └── mapper/ # MyBatis mapper
|
||||||
|
│ └── {Module}ApplicationService.java # Application service layer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
### Build and Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build entire project
|
||||||
|
./mvnw clean install
|
||||||
|
|
||||||
|
# Build specific module
|
||||||
|
./mvnw clean package -pl agileboot-admin -am
|
||||||
|
|
||||||
|
# Build with tests (tests are skipped by default)
|
||||||
|
./mvnw clean package -pl agileboot-admin -am -DskipTests=false
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
./mvnw test
|
||||||
|
./mvnw test -pl agileboot-domain # Test specific module
|
||||||
|
./mvnw test -Dtest=UserModelTest # Run single test class
|
||||||
|
|
||||||
|
# Use build script
|
||||||
|
build.bat
|
||||||
|
|
||||||
|
# Run admin module
|
||||||
|
cd agileboot-admin
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
|
||||||
|
# Or run with embedded database/Redis
|
||||||
|
java -jar agileboot-admin/target/agileboot-admin.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Profiles
|
||||||
|
|
||||||
|
**Configuration file**: `agileboot-admin/src/main/resources/application.yml`
|
||||||
|
|
||||||
|
- **dev**: Development profile (MySQL + Redis required)
|
||||||
|
- **test**: Test profile (embedded H2 + embedded Redis)
|
||||||
|
- **prod**: Production profile
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spring:
|
||||||
|
profiles:
|
||||||
|
active: basic,dev # Change to basic,test for embedded services
|
||||||
|
|
||||||
|
agileboot:
|
||||||
|
embedded:
|
||||||
|
mysql: true # Set to true to use H2 (no external DB needed)
|
||||||
|
redis: true # Set to true to use embedded Redis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Startup classes:**
|
||||||
|
- Admin module: `com.agileboot.admin.AgileBootAdminApplication`
|
||||||
|
- API module: `com.agileboot.api.AgileBooApiApplication`
|
||||||
|
- Integration tests: `com.agileboot.integrationTest.IntegrationTestApplication`
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With external MySQL and Redis (default dev profile)
|
||||||
|
1. Import database: sql/agileboot_*.sql (latest version)
|
||||||
|
2. Configure: agileboot-admin/src/main/resources/application-dev.yml
|
||||||
|
3. Run: ./mvnw spring-boot:run -pl agileboot-admin
|
||||||
|
|
||||||
|
# Without external dependencies (test profile)
|
||||||
|
1. Change spring.profiles.active to: basic,test
|
||||||
|
2. Set agileboot.embedded.mysql: true
|
||||||
|
3. Set agileboot.embedded.redis: true
|
||||||
|
4. Run: ./mvnw spring-boot:run -pl agileboot-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access points:**
|
||||||
|
- API Documentation: http://localhost:8080/v3/api-docs
|
||||||
|
- Druid Monitor: http://localhost:8080/druid/ (admin/123456)
|
||||||
|
- Login credentials: admin/admin123
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Request Flow (CQRS)
|
||||||
|
|
||||||
|
**Queries**: Controller → `{Module}Query` → `{Module}ApplicationService` → `{Module}Service` (Db) → `{Module}Mapper`
|
||||||
|
|
||||||
|
**Commands**: Controller → `{Module}Command` → `{Module}ApplicationService` → `{Module}Model` → save/update
|
||||||
|
|
||||||
|
### Module Organization
|
||||||
|
|
||||||
|
Each business domain follows DDD structure. Example: `agileboot-domain/src/main/java/com/agileboot/domain/{module}/`
|
||||||
|
|
||||||
|
- **command/**: Command objects for data updates (Create/Update/Delete)
|
||||||
|
- **query/**: Query objects for data retrieval
|
||||||
|
- **dto/**: Data transfer objects for API responses
|
||||||
|
- **model/**: Domain models with business logic
|
||||||
|
- **db/**: Data access layer (entity, service, mapper)
|
||||||
|
- **ApplicationService**: Transaction script layer, orchestrates domain models
|
||||||
|
|
||||||
|
## Business Modules
|
||||||
|
|
||||||
|
Core modules in `agileboot-domain`:
|
||||||
|
- **system**: User, role, menu, dept, config, log, notice, post
|
||||||
|
- **cabinet**: Smart cabinet device management
|
||||||
|
- **shop**: E-commerce (products, orders)
|
||||||
|
- **wx**: WeChat user integration
|
||||||
|
- **qywx**: Enterprise WeChat integration (supports corpid)
|
||||||
|
- **ab98**: User management with tags and balance
|
||||||
|
- **mqtt**: MQTT server integration
|
||||||
|
- **asset**: Asset management
|
||||||
|
|
||||||
|
## Caching System
|
||||||
|
|
||||||
|
**Multi-level cache architecture:**
|
||||||
|
1. **Caffeine** (local): User, role, post, login user cache
|
||||||
|
2. **Guava** (config): System config, dict data, dept data
|
||||||
|
3. **Redis** (distributed, optional): Session, captcha, distributed locks
|
||||||
|
|
||||||
|
**Key classes:**
|
||||||
|
- `agileboot-infrastructure/src/main/java/.../cache/`
|
||||||
|
- `CacheCenter`: Cache management
|
||||||
|
- `AbstractCaffeineCacheTemplate`: Caffeine cache template
|
||||||
|
|
||||||
|
## Key Configuration Files
|
||||||
|
|
||||||
|
- `pom.xml`: Maven parent POM with all dependencies
|
||||||
|
- `GoogleStyle.xml`: Code formatting template (required for IntelliJ)
|
||||||
|
- `application.yml`: Main config (profiles, embedded services)
|
||||||
|
- `application-dev.yml`: Dev database/Redis config
|
||||||
|
- `application-test.yml`: Test with H2/embedded Redis
|
||||||
|
- `sql/`: Database migration scripts (latest first)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**Test structure**: Each module has `src/test/java/`
|
||||||
|
- Unit tests in `*Test.java`
|
||||||
|
- Integration tests in `*IntegrationTest.java`
|
||||||
|
|
||||||
|
**Running tests:**
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
./mvnw test
|
||||||
|
|
||||||
|
# Specific module
|
||||||
|
./mvnw test -pl agileboot-domain
|
||||||
|
|
||||||
|
# Single test class
|
||||||
|
./mvnw test -Dtest=UserModelTest
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
./mvnw jacoco:report # If JaCoCo configured
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Tests are skipped by default in build (`<skipTests>true</skipTests>` in parent POM). Set `-DskipTests=false` to run them.
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
**Required:**
|
||||||
|
- Import `GoogleStyle.xml` into IntelliJ: Settings → Editor → Code Style → Java → Import Schema
|
||||||
|
- Properties files encoding: Settings → Editor → File Encodings → Properties Files → Set to UTF-8
|
||||||
|
- Use enums instead of dictionary type data
|
||||||
|
- Centralized error handling with error codes
|
||||||
|
- Write unit tests for business logic
|
||||||
|
|
||||||
|
**IDE Setup:**
|
||||||
|
- **IntelliJ IDEA** (recommended)
|
||||||
|
- Lombok plugin installed
|
||||||
|
- Google code style applied
|
||||||
|
|
||||||
|
## Important Business Features
|
||||||
|
|
||||||
|
### Enterprise WeChat Integration
|
||||||
|
- Supports `corpid` field for multi-tenant enterprise WeChat
|
||||||
|
- Module: `agileboot-domain/qywx/`
|
||||||
|
- Documentation: `doc/智能柜系统指南.md`
|
||||||
|
|
||||||
|
### Smart Cabinet System
|
||||||
|
- Device management with cabinet/cell hierarchy
|
||||||
|
- Real-time cell status monitoring
|
||||||
|
- Operation logging (open/close/lock)
|
||||||
|
- Module: `agileboot-domain/cabinet/` and `agileboot-admin/controller/cabinet/`
|
||||||
|
|
||||||
|
### WeChat Pay Integration
|
||||||
|
- JSAPI payment support
|
||||||
|
- Refund functionality
|
||||||
|
- Module: `agileboot-domain/shop/`
|
||||||
|
- Documentation: `doc/微信支付集成指南.md`
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
**Core tables** (~10 core tables in AgileBoot):
|
||||||
|
- `sys_user`, `sys_role`, `sys_menu`, `sys_dept`
|
||||||
|
- `sys_config`, `sys_dict_type`, `sys_notice`
|
||||||
|
- Plus business tables (cabinet, shop, ab98_user, etc.)
|
||||||
|
|
||||||
|
**Migration files** in `sql/` directory (latest first):
|
||||||
|
- `agileboot-*.sql`: Base schema
|
||||||
|
- `YYYYMMDD_*.sql`: Feature migrations (e.g., 20251029_wx_user.sql)
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **New Feature**:
|
||||||
|
- Create domain module in `agileboot-domain/{feature}/`
|
||||||
|
- Implement DDD structure (command, query, model, db, service)
|
||||||
|
- Add controllers in `agileboot-admin/controller/{feature}/`
|
||||||
|
- Write unit tests
|
||||||
|
- Update database with migration script
|
||||||
|
|
||||||
|
2. **Database Changes**:
|
||||||
|
- Create new migration: `sql/YYYYMMDD_feature_name.sql`
|
||||||
|
- Update CodeGenerator if needed
|
||||||
|
- Test with test profile (H2 + embedded Redis)
|
||||||
|
|
||||||
|
3. **API Development**:
|
||||||
|
- Document with `@Operation` annotation
|
||||||
|
- Use proper DTOs
|
||||||
|
- Add to `application.yml` springdoc group-configs if needed
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Port 8080 busy**: Change `server.port` in `application.yml`
|
||||||
|
|
||||||
|
**Redis port conflict**: Modify `spring.redis.port` in `application-dev.yml`
|
||||||
|
|
||||||
|
**MacOS embedded Redis**: High versions may not support embedded Redis - use external Redis
|
||||||
|
|
||||||
|
**Build fails**: Check Java version (requires Java 8+), Maven 3.9+, ensure `./mvnw clean install` succeeds
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **README.md**: Project overview and setup guide
|
||||||
|
- **doc/项目概述.md**: Detailed architecture overview
|
||||||
|
- **doc/智能柜系统指南.md**: Smart cabinet system guide
|
||||||
|
- **doc/缓存系统指南.md**: Caching system guide
|
||||||
|
- **doc/微信支付集成指南.md**: WeChat Pay integration guide
|
||||||
|
|
||||||
|
## Recent Commits (for context)
|
||||||
|
|
||||||
|
```
|
||||||
|
eb41f35 feat(wx): 添加微信小程序登录功能支持
|
||||||
|
9562d1c 登录接口更新用户登录信息修复
|
||||||
|
7cd08c1 refactor(docs): 重构文档结构并迁移docker安装指南
|
||||||
|
e53ff77 feat(智能柜): 添加corpid字段支持企业微信集成
|
||||||
|
ddc3c91 feat: 添加企业微信用户ID缓存功能
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Use test profile for quick development without external dependencies
|
||||||
|
- Check `application-dev.yml` for database/Redis configuration
|
||||||
|
- Druid monitor at `/druid/` shows SQL performance
|
||||||
|
- API docs available at `/v3/api-docs` and `/swagger-ui.html`
|
||||||
|
- Code style: Strictly follow GoogleStyle.xml formatting
|
||||||
|
- All business logic should have unit tests
|
||||||
|
- Domain models contain business logic, ApplicationService is transaction script layer
|
||||||
|
- Use multi-level cache for performance-critical data
|
||||||
|
|
@ -5,12 +5,18 @@ import cn.hutool.json.JSONUtil;
|
||||||
import com.agileboot.common.core.dto.ResponseDTO;
|
import com.agileboot.common.core.dto.ResponseDTO;
|
||||||
import com.agileboot.common.exception.ApiException;
|
import com.agileboot.common.exception.ApiException;
|
||||||
import com.agileboot.common.exception.error.ErrorCode;
|
import com.agileboot.common.exception.error.ErrorCode;
|
||||||
|
import com.agileboot.domain.common.cache.CacheCenter;
|
||||||
import com.agileboot.domain.wx.WxService;
|
import com.agileboot.domain.wx.WxService;
|
||||||
|
import com.agileboot.domain.wx.user.WxUserApplicationService;
|
||||||
|
import com.agileboot.domain.wx.user.dto.WxUserDTO;
|
||||||
|
import com.agileboot.domain.wx.utils.DynamicCodeGenerator;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.client.RestClientException;
|
import org.springframework.web.client.RestClientException;
|
||||||
|
|
||||||
|
|
@ -25,6 +31,8 @@ import java.util.Map;
|
||||||
public class WxController {
|
public class WxController {
|
||||||
private final WxService wxService;
|
private final WxService wxService;
|
||||||
|
|
||||||
|
private final WxUserApplicationService wxUserApplicationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取小程序用户OpenID
|
* 获取小程序用户OpenID
|
||||||
* @param code 微信授权码
|
* @param code 微信授权码
|
||||||
|
|
@ -32,14 +40,83 @@ public class WxController {
|
||||||
* @throws ApiException 当code无效或微信接口调用失败时抛出
|
* @throws ApiException 当code无效或微信接口调用失败时抛出
|
||||||
*/
|
*/
|
||||||
@GetMapping("/mpCodeToOpenId")
|
@GetMapping("/mpCodeToOpenId")
|
||||||
public ResponseDTO<String> mpCodeToOpenId(String code) {
|
public ResponseDTO<WxUserDTO> mpCodeToOpenId(String code) {
|
||||||
try {
|
try {
|
||||||
String openid = wxService.getSmallOpenid(code);
|
String openid = wxService.getSmallOpenid(code);
|
||||||
|
// 校验openid是否为空
|
||||||
|
if (StringUtils.isBlank(openid)) {
|
||||||
|
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "根据code获取openid失败");
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseDTO.ok(openid);
|
return ResponseDTO.ok(wxUserApplicationService.getOrCreateUserByOpenid(openid));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取openid失败", e);
|
log.error("获取openid失败", e);
|
||||||
return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, e.getMessage()));
|
return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据openid获取微信用户信息
|
||||||
|
* @param openid 微信用户openid
|
||||||
|
* @return 微信用户信息
|
||||||
|
* @throws ApiException 当openid无效或用户不存在时抛出
|
||||||
|
*/
|
||||||
|
@GetMapping("/getWxUserByOpenid")
|
||||||
|
public ResponseDTO<WxUserDTO> getWxUserByOpenid(@RequestParam("openid") String openid) {
|
||||||
|
try {
|
||||||
|
WxUserDTO wxUserDTO = wxUserApplicationService.getUserDetailByOpenid(openid);
|
||||||
|
|
||||||
|
if (wxUserDTO == null) {
|
||||||
|
return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "用户不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseDTO.ok(wxUserDTO);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取微信用户信息失败", e);
|
||||||
|
return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据openid生成动态码
|
||||||
|
* @param openid 微信用户openid
|
||||||
|
* @return 包含动态码的响应结果
|
||||||
|
* @throws ApiException 当openid无效或用户不存在时抛出
|
||||||
|
*/
|
||||||
|
@GetMapping("/generateDynamicCode")
|
||||||
|
public ResponseDTO<Map<String, String>> generateDynamicCode(@RequestParam("openid") String openid) {
|
||||||
|
try {
|
||||||
|
// 校验openid是否为空
|
||||||
|
if (StringUtils.isBlank(openid)) {
|
||||||
|
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "openid不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查该openid对应的用户是否存在
|
||||||
|
WxUserDTO wxUserDTO = wxUserApplicationService.getUserDetailByOpenid(openid);
|
||||||
|
if (wxUserDTO == null) {
|
||||||
|
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "用户不存在,无法生成动态码");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一的6位数字动态码(使用动态码作为缓存键,自动应用前缀)
|
||||||
|
String dynamicCode = DynamicCodeGenerator.generateUniqueDynamicCodeWithPrefix(
|
||||||
|
cacheKey -> CacheCenter.dynamicCodeCache.get(cacheKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 构建带前缀的缓存键并保存openid
|
||||||
|
String cacheKey = DynamicCodeGenerator.buildCacheKey(dynamicCode);
|
||||||
|
CacheCenter.dynamicCodeCache.put(cacheKey, openid);
|
||||||
|
|
||||||
|
log.info("为openid {} 生成唯一动态码: {},已保存到缓存(键: {})", openid, dynamicCode, cacheKey);
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
result.put("dynamicCode", dynamicCode);
|
||||||
|
result.put("validityMinutes", String.valueOf(DynamicCodeGenerator.getCodeValidityMinutes()));
|
||||||
|
|
||||||
|
return ResponseDTO.ok(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成动态码失败", e);
|
||||||
|
return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
package com.agileboot.api.controller;
|
package com.agileboot.api.controller;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.agileboot.common.core.dto.ResponseDTO;
|
import com.agileboot.common.core.dto.ResponseDTO;
|
||||||
import com.agileboot.common.exception.ApiException;
|
import com.agileboot.common.exception.ApiException;
|
||||||
import com.agileboot.common.exception.error.ErrorCode;
|
import com.agileboot.common.exception.error.ErrorCode;
|
||||||
|
import com.agileboot.domain.ab98.api.Ab98ApiUtil;
|
||||||
import com.agileboot.domain.ab98.api.SsoLoginUserinfo;
|
import com.agileboot.domain.ab98.api.SsoLoginUserinfo;
|
||||||
import com.agileboot.domain.ab98.user.Ab98UserApplicationService;
|
import com.agileboot.domain.ab98.user.Ab98UserApplicationService;
|
||||||
import com.agileboot.domain.ab98.user.command.BindQyUserCommand;
|
import com.agileboot.domain.ab98.user.command.BindQyUserCommand;
|
||||||
|
import com.agileboot.domain.ab98.user.command.BindWxMpUserCommand;
|
||||||
import com.agileboot.domain.ab98.user.db.Ab98UserEntity;
|
import com.agileboot.domain.ab98.user.db.Ab98UserEntity;
|
||||||
import com.agileboot.domain.qywx.user.QyUserApplicationService;
|
import com.agileboot.domain.qywx.user.QyUserApplicationService;
|
||||||
|
import com.agileboot.domain.wx.user.WxUserApplicationService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import com.agileboot.domain.ab98.api.Ab98ApiUtil;
|
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
|
|
@ -29,6 +31,7 @@ public class WxLoginController {
|
||||||
private final QyUserApplicationService qyUserApplicationService;
|
private final QyUserApplicationService qyUserApplicationService;
|
||||||
private final Ab98UserApplicationService ab98UserApplicationService;
|
private final Ab98UserApplicationService ab98UserApplicationService;
|
||||||
private final Ab98UserApplicationService userApplicationService;
|
private final Ab98UserApplicationService userApplicationService;
|
||||||
|
private final WxUserApplicationService wxUserApplicationService;
|
||||||
|
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
@ApiOperation(value = "用户退出登录")
|
@ApiOperation(value = "用户退出登录")
|
||||||
|
|
@ -166,4 +169,25 @@ public class WxLoginController {
|
||||||
Ab98UserEntity ab98User = userApplicationService.bindQyUser(command);
|
Ab98UserEntity ab98User = userApplicationService.bindQyUser(command);
|
||||||
return ResponseDTO.ok(ab98User);
|
return ResponseDTO.ok(ab98User);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/bindWxMpUser")
|
||||||
|
@ApiOperation(value = "绑定微信小程序用户到汇邦云", notes = "通过动态码、姓名、身份证绑定微信小程序用户到汇邦云")
|
||||||
|
public ResponseDTO<String> bindWxMpUser(@RequestBody BindWxMpUserCommand command) {
|
||||||
|
if (command == null || StringUtils.isBlank(command.getDynamicCode()) || StringUtils.isBlank(command.getName()) || StringUtils.isBlank(command.getIdNum())) {
|
||||||
|
log.error("绑定微信小程序用户到汇邦云参数错误: {}", JSONUtil.toJsonStr(command));
|
||||||
|
return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "参数错误"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean success = wxUserApplicationService.bindWxMpUser(command);
|
||||||
|
if (success) {
|
||||||
|
return ResponseDTO.ok("绑定成功");
|
||||||
|
} else {
|
||||||
|
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "绑定失败"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("绑定微信小程序用户到汇邦云失败: ", e);
|
||||||
|
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "绑定失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@ public enum ErrorCode implements ErrorCodeInterface {
|
||||||
|
|
||||||
USER_ADMIN_CAN_NOT_BE_MODIFY(10515, "管理员不允许做任何修改", "Business.USER_ADMIN_CAN_NOT_BE_MODIFY"),
|
USER_ADMIN_CAN_NOT_BE_MODIFY(10515, "管理员不允许做任何修改", "Business.USER_ADMIN_CAN_NOT_BE_MODIFY"),
|
||||||
|
|
||||||
|
COMMON_BAD_REQUEST(10516, "请求参数错误", "Business.COMMON_BAD_REQUEST"),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.agileboot.domain.ab98.user.command;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BindWxMpUserCommand {
|
||||||
|
private String dynamicCode;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String idNum;
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,8 @@ public class CacheCenter {
|
||||||
|
|
||||||
public static AbstractCaffeineCacheTemplate<String> qyUseridCache;
|
public static AbstractCaffeineCacheTemplate<String> qyUseridCache;
|
||||||
|
|
||||||
|
public static AbstractCaffeineCacheTemplate<String> dynamicCodeCache;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
|
GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
|
||||||
|
|
@ -50,6 +52,7 @@ public class CacheCenter {
|
||||||
roleCache = caffeineCache.roleCache;
|
roleCache = caffeineCache.roleCache;
|
||||||
postCache = caffeineCache.postCache;
|
postCache = caffeineCache.postCache;
|
||||||
qyUseridCache = caffeineCache.qyUseridCache;
|
qyUseridCache = caffeineCache.qyUseridCache;
|
||||||
|
dynamicCodeCache = caffeineCache.dynamicCodeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ import org.springframework.stereotype.Component;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CaffeineCacheService {
|
public class CaffeineCacheService {
|
||||||
|
|
||||||
public AbstractCaffeineCacheTemplate<String> captchaCache = new AbstractCaffeineCacheTemplate<String>() {
|
// 验证码缓存:5分钟过期和刷新(验证码通常短时间内有效)
|
||||||
|
public AbstractCaffeineCacheTemplate<String> captchaCache = new AbstractCaffeineCacheTemplate<String>(
|
||||||
|
5, java.util.concurrent.TimeUnit.MINUTES,
|
||||||
|
5, java.util.concurrent.TimeUnit.MINUTES) {
|
||||||
@Override
|
@Override
|
||||||
public String getObjectFromDb(Object id) {
|
public String getObjectFromDb(Object id) {
|
||||||
// 验证码通常不需要从数据库获取,这里返回null
|
// 验证码通常不需要从数据库获取,这里返回null
|
||||||
|
|
@ -29,7 +32,10 @@ public class CaffeineCacheService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public AbstractCaffeineCacheTemplate<SystemLoginUser> loginUserCache = new AbstractCaffeineCacheTemplate<SystemLoginUser>() {
|
// 登录用户缓存:1小时过期和刷新(登录态保持)
|
||||||
|
public AbstractCaffeineCacheTemplate<SystemLoginUser> loginUserCache = new AbstractCaffeineCacheTemplate<SystemLoginUser>(
|
||||||
|
12, java.util.concurrent.TimeUnit.HOURS,
|
||||||
|
12, java.util.concurrent.TimeUnit.HOURS) {
|
||||||
@Override
|
@Override
|
||||||
public SystemLoginUser getObjectFromDb(Object id) {
|
public SystemLoginUser getObjectFromDb(Object id) {
|
||||||
// 登录用户信息通常不需要从数据库获取,这里返回null
|
// 登录用户信息通常不需要从数据库获取,这里返回null
|
||||||
|
|
@ -37,7 +43,10 @@ public class CaffeineCacheService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public AbstractCaffeineCacheTemplate<SysUserEntity> userCache = new AbstractCaffeineCacheTemplate<SysUserEntity>() {
|
// 用户信息缓存:2小时过期和刷新(用户信息相对稳定但可能更新)
|
||||||
|
public AbstractCaffeineCacheTemplate<SysUserEntity> userCache = new AbstractCaffeineCacheTemplate<SysUserEntity>(
|
||||||
|
12, java.util.concurrent.TimeUnit.HOURS,
|
||||||
|
12, java.util.concurrent.TimeUnit.HOURS) {
|
||||||
@Override
|
@Override
|
||||||
public SysUserEntity getObjectFromDb(Object id) {
|
public SysUserEntity getObjectFromDb(Object id) {
|
||||||
SysUserService userService = cn.hutool.extra.spring.SpringUtil.getBean(SysUserService.class);
|
SysUserService userService = cn.hutool.extra.spring.SpringUtil.getBean(SysUserService.class);
|
||||||
|
|
@ -45,7 +54,10 @@ public class CaffeineCacheService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public AbstractCaffeineCacheTemplate<SysRoleEntity> roleCache = new AbstractCaffeineCacheTemplate<SysRoleEntity>() {
|
// 角色缓存:24小时过期和刷新(角色信息很少变更)
|
||||||
|
public AbstractCaffeineCacheTemplate<SysRoleEntity> roleCache = new AbstractCaffeineCacheTemplate<SysRoleEntity>(
|
||||||
|
24, java.util.concurrent.TimeUnit.HOURS,
|
||||||
|
24, java.util.concurrent.TimeUnit.HOURS) {
|
||||||
@Override
|
@Override
|
||||||
public SysRoleEntity getObjectFromDb(Object id) {
|
public SysRoleEntity getObjectFromDb(Object id) {
|
||||||
SysRoleService roleService = cn.hutool.extra.spring.SpringUtil.getBean(SysRoleService.class);
|
SysRoleService roleService = cn.hutool.extra.spring.SpringUtil.getBean(SysRoleService.class);
|
||||||
|
|
@ -53,7 +65,10 @@ public class CaffeineCacheService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public AbstractCaffeineCacheTemplate<SysPostEntity> postCache = new AbstractCaffeineCacheTemplate<SysPostEntity>() {
|
// 岗位缓存:24小时过期和刷新(岗位信息很少变更)
|
||||||
|
public AbstractCaffeineCacheTemplate<SysPostEntity> postCache = new AbstractCaffeineCacheTemplate<SysPostEntity>(
|
||||||
|
24, java.util.concurrent.TimeUnit.HOURS,
|
||||||
|
24, java.util.concurrent.TimeUnit.HOURS) {
|
||||||
@Override
|
@Override
|
||||||
public SysPostEntity getObjectFromDb(Object id) {
|
public SysPostEntity getObjectFromDb(Object id) {
|
||||||
SysPostService postService = cn.hutool.extra.spring.SpringUtil.getBean(SysPostService.class);
|
SysPostService postService = cn.hutool.extra.spring.SpringUtil.getBean(SysPostService.class);
|
||||||
|
|
@ -61,7 +76,10 @@ public class CaffeineCacheService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public AbstractCaffeineCacheTemplate<String> qyUseridCache = new AbstractCaffeineCacheTemplate<String>() {
|
// 企业微信用户ID缓存:12小时过期和刷新(通过API获取,有一定成本)
|
||||||
|
public AbstractCaffeineCacheTemplate<String> qyUseridCache = new AbstractCaffeineCacheTemplate<String>(
|
||||||
|
12, java.util.concurrent.TimeUnit.HOURS,
|
||||||
|
12, java.util.concurrent.TimeUnit.HOURS) {
|
||||||
@Override
|
@Override
|
||||||
public String getObjectFromDb(Object id) {
|
public String getObjectFromDb(Object id) {
|
||||||
// 企业微信用户ID需要通过API获取,这里返回null,由调用方处理
|
// 企业微信用户ID需要通过API获取,这里返回null,由调用方处理
|
||||||
|
|
@ -69,6 +87,17 @@ public class CaffeineCacheService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 动态码缓存:30分钟过期和刷新(动态码通常短时间内有效)
|
||||||
|
public AbstractCaffeineCacheTemplate<String> dynamicCodeCache = new AbstractCaffeineCacheTemplate<String>(
|
||||||
|
30, java.util.concurrent.TimeUnit.MINUTES,
|
||||||
|
30, java.util.concurrent.TimeUnit.MINUTES) {
|
||||||
|
@Override
|
||||||
|
public String getObjectFromDb(Object id) {
|
||||||
|
// 动态码不需要从数据库获取,这里返回null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取缓存统计信息
|
* 获取缓存统计信息
|
||||||
* @return 统计信息字符串
|
* @return 统计信息字符串
|
||||||
|
|
@ -82,6 +111,7 @@ public class CaffeineCacheService {
|
||||||
stats.append("Role Cache: ").append(roleCache.getStats()).append("\n");
|
stats.append("Role Cache: ").append(roleCache.getStats()).append("\n");
|
||||||
stats.append("Post Cache: ").append(postCache.getStats()).append("\n");
|
stats.append("Post Cache: ").append(postCache.getStats()).append("\n");
|
||||||
stats.append("QyUserid Cache: ").append(qyUseridCache.getStats()).append("\n");
|
stats.append("QyUserid Cache: ").append(qyUseridCache.getStats()).append("\n");
|
||||||
|
stats.append("Dynamic Code Cache: ").append(dynamicCodeCache.getStats()).append("\n");
|
||||||
return stats.toString();
|
return stats.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
package com.agileboot.domain.wx.user;
|
||||||
|
|
||||||
|
import com.agileboot.common.core.page.PageDTO;
|
||||||
|
import com.agileboot.domain.ab98.api.Ab98ApiUtil;
|
||||||
|
import com.agileboot.domain.ab98.api.Ab98UserDto;
|
||||||
|
import com.agileboot.domain.ab98.user.Ab98UserApplicationService;
|
||||||
|
import com.agileboot.domain.ab98.user.command.AddAb98UserCommand;
|
||||||
|
import com.agileboot.domain.ab98.user.command.BindWxMpUserCommand;
|
||||||
|
import com.agileboot.domain.ab98.user.command.UpdateAb98UserCommand;
|
||||||
|
import com.agileboot.domain.ab98.user.db.Ab98UserEntity;
|
||||||
|
import com.agileboot.domain.ab98.user.db.Ab98UserService;
|
||||||
|
import com.agileboot.domain.common.cache.CacheCenter;
|
||||||
|
import com.agileboot.domain.common.command.BulkOperationCommand;
|
||||||
|
import com.agileboot.domain.wx.utils.DynamicCodeGenerator;
|
||||||
|
import com.agileboot.domain.wx.utils.WxNicknameGenerator;
|
||||||
|
import com.agileboot.domain.wx.user.command.AddWxUserCommand;
|
||||||
|
import com.agileboot.domain.wx.user.command.UpdateWxUserCommand;
|
||||||
|
import com.agileboot.domain.wx.user.db.SearchWxUserDO;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserEntity;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserService;
|
||||||
|
import com.agileboot.domain.wx.user.dto.WxUserDTO;
|
||||||
|
import com.agileboot.domain.wx.user.model.WxUserModel;
|
||||||
|
import com.agileboot.domain.wx.user.model.WxUserModelFactory;
|
||||||
|
import com.agileboot.domain.wx.user.query.SearchWxUserQuery;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户应用服务
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxUserApplicationService {
|
||||||
|
|
||||||
|
private final WxUserService userService;
|
||||||
|
|
||||||
|
private final WxUserModelFactory modelFactory;
|
||||||
|
|
||||||
|
private final Ab98UserService ab98UserService;
|
||||||
|
|
||||||
|
private final Ab98UserApplicationService ab98UserApplicationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信用户列表(分页)
|
||||||
|
*/
|
||||||
|
public PageDTO<WxUserDTO> getUserList(SearchWxUserQuery<SearchWxUserDO> query) {
|
||||||
|
Page<SearchWxUserDO> userPage = userService.getUserListWithJoin(query);
|
||||||
|
List<WxUserDTO> userDTOList = userPage.getRecords()
|
||||||
|
.stream()
|
||||||
|
.map(WxUserDTO::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return new PageDTO<>(userDTOList, userPage.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信用户详情
|
||||||
|
*/
|
||||||
|
public WxUserDTO getUserDetailInfo(Long wxUserId) {
|
||||||
|
WxUserEntity userEntity = userService.getById(wxUserId);
|
||||||
|
if (userEntity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new WxUserDTO(userEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据openid获取微信用户详情
|
||||||
|
*/
|
||||||
|
public WxUserDTO getUserDetailByOpenid(String openid) {
|
||||||
|
WxUserEntity userEntity = userService.getByOpenid(openid);
|
||||||
|
if (userEntity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new WxUserDTO(userEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加微信用户
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void addUser(AddWxUserCommand command) {
|
||||||
|
WxUserModel model = modelFactory.create();
|
||||||
|
model.loadAddWxUserCommand(command);
|
||||||
|
|
||||||
|
// 业务校验
|
||||||
|
model.validateBeforeSave();
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
model.insert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新微信用户
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateUser(UpdateWxUserCommand command) {
|
||||||
|
WxUserModel model = modelFactory.loadById(command.getWxUserId());
|
||||||
|
model.loadUpdateWxUserCommand(command);
|
||||||
|
|
||||||
|
// 业务校验
|
||||||
|
model.checkOpenidIsUnique();
|
||||||
|
model.checkTelIsUnique();
|
||||||
|
model.checkFieldRelatedEntityExist();
|
||||||
|
model.checkBalanceIsValid();
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
model.updateById();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除微信用户(单个)
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteUser(Long wxUserId) {
|
||||||
|
WxUserModel model = modelFactory.loadById(wxUserId);
|
||||||
|
|
||||||
|
// 业务校验
|
||||||
|
model.checkCanBeDelete();
|
||||||
|
|
||||||
|
// 软删除
|
||||||
|
model.deleteById();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除微信用户
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteUsers(BulkOperationCommand<Long> command) {
|
||||||
|
for (Long wxUserId : command.getIds()) {
|
||||||
|
WxUserModel model = modelFactory.loadById(wxUserId);
|
||||||
|
model.checkCanBeDelete();
|
||||||
|
model.deleteById();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加余额
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void increaseBalance(Long wxUserId, Integer amount) {
|
||||||
|
WxUserModel model = modelFactory.loadById(wxUserId);
|
||||||
|
|
||||||
|
model.increaseBalance(amount);
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
model.updateById();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 减少余额
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void decreaseBalance(Long wxUserId, Integer amount) {
|
||||||
|
WxUserModel model = modelFactory.loadById(wxUserId);
|
||||||
|
|
||||||
|
model.decreaseBalance(amount);
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
model.updateById();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置余额
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void setBalance(Long wxUserId, Integer balance) {
|
||||||
|
WxUserModel model = modelFactory.loadById(wxUserId);
|
||||||
|
|
||||||
|
model.setWxBalance(balance);
|
||||||
|
|
||||||
|
// 校验
|
||||||
|
model.checkBalanceIsValid();
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
model.updateById();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据openid获取或创建微信用户
|
||||||
|
* 如果openid对应数据不存在,则创建一条新数据到数据库
|
||||||
|
*
|
||||||
|
* @param openid 微信openid
|
||||||
|
* @return 微信用户信息
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public WxUserDTO getOrCreateUserByOpenid(String openid) {
|
||||||
|
// 先尝试查询用户
|
||||||
|
WxUserDTO userDTO = getUserDetailByOpenid(openid);
|
||||||
|
|
||||||
|
// 如果用户不存在,则创建新用户
|
||||||
|
if (userDTO == null) {
|
||||||
|
AddWxUserCommand command = new AddWxUserCommand();
|
||||||
|
command.setOpenid(openid);
|
||||||
|
command.setNickName(WxNicknameGenerator.generateRandomNickname()); // 使用昵称生成器生成随机昵称
|
||||||
|
|
||||||
|
// 保存新用户
|
||||||
|
WxUserModel model = modelFactory.create();
|
||||||
|
model.loadAddWxUserCommand(command);
|
||||||
|
|
||||||
|
// 业务校验(只校验openid唯一性,因为是新用户,其他字段有默认值)
|
||||||
|
model.checkOpenidIsUnique();
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
model.insert();
|
||||||
|
|
||||||
|
// 返回新创建的用户信息
|
||||||
|
userDTO = getUserDetailByOpenid(openid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定微信小程序用户到汇邦云
|
||||||
|
*
|
||||||
|
* @param command 绑定命令(包含动态码、姓名、身份证)
|
||||||
|
* @return 绑定是否成功
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean bindWxMpUser(BindWxMpUserCommand command) {
|
||||||
|
if (command == null || StringUtils.isBlank(command.getDynamicCode())
|
||||||
|
|| StringUtils.isBlank(command.getName())
|
||||||
|
|| StringUtils.isBlank(command.getIdNum())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从动态码缓存中获取openid
|
||||||
|
String cacheKey = DynamicCodeGenerator.buildCacheKey(command.getDynamicCode());
|
||||||
|
String openid = CacheCenter.dynamicCodeCache.get(cacheKey);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(openid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询汇邦云用户(通过身份证)
|
||||||
|
Ab98UserEntity ab98UserEntity = ab98UserService.getByIdnum(command.getIdNum());
|
||||||
|
|
||||||
|
// 如果汇邦云用户存在且姓名匹配,则直接绑定
|
||||||
|
if (ab98UserEntity != null && StringUtils.equals(ab98UserEntity.getName(), command.getName())) {
|
||||||
|
// 更新微信用户表的ab98UserId
|
||||||
|
WxUserEntity wxUserEntity = userService.getByOpenid(openid);
|
||||||
|
if (wxUserEntity != null) {
|
||||||
|
wxUserEntity.setAb98UserId(ab98UserEntity.getAb98UserId());
|
||||||
|
userService.updateById(wxUserEntity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 汇邦云用户不存在或姓名不匹配,调用Ab98ApiUtil获取用户信息
|
||||||
|
Ab98UserDto ab98UserDto = Ab98ApiUtil.pullUserInfoByIdnum("wxshop", "34164e41f0c6694be6bbbba0dc50c14a", command.getIdNum());
|
||||||
|
if (ab98UserDto == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证姓名是否匹配
|
||||||
|
if (!StringUtils.equals(ab98UserDto.getRealName(), command.getName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建AddAb98UserCommand
|
||||||
|
AddAb98UserCommand addCommand = new AddAb98UserCommand();
|
||||||
|
addCommand.setUserid(ab98UserDto.getSsoUid());
|
||||||
|
addCommand.setName(ab98UserDto.getRealName());
|
||||||
|
addCommand.setTel(ab98UserDto.getPhone());
|
||||||
|
addCommand.setSex(ab98UserDto.getSex());
|
||||||
|
addCommand.setIdnum(ab98UserDto.getIdCardNo());
|
||||||
|
addCommand.setIdcardFront(ab98UserDto.getIdCardFront());
|
||||||
|
addCommand.setIdcardBack(ab98UserDto.getIdCardBack());
|
||||||
|
addCommand.setFaceImg(ab98UserDto.getFacePicture());
|
||||||
|
addCommand.setAddress(ab98UserDto.getAddress());
|
||||||
|
addCommand.setRegistered(true);
|
||||||
|
addCommand.initBaseEntity();
|
||||||
|
|
||||||
|
// 检查汇邦云用户是否已存在,如果存在则更新,否则创建
|
||||||
|
Ab98UserEntity existingUser = ab98UserService.getByIdnum(command.getIdNum());
|
||||||
|
if (existingUser != null) {
|
||||||
|
// 更新现有用户
|
||||||
|
UpdateAb98UserCommand updateCommand = new UpdateAb98UserCommand();
|
||||||
|
updateCommand.setAb98UserId(existingUser.getAb98UserId());
|
||||||
|
updateCommand.setUserid(ab98UserDto.getSsoUid());
|
||||||
|
updateCommand.setName(ab98UserDto.getRealName());
|
||||||
|
updateCommand.setTel(ab98UserDto.getPhone());
|
||||||
|
updateCommand.setSex(ab98UserDto.getSex());
|
||||||
|
updateCommand.setIdnum(ab98UserDto.getIdCardNo());
|
||||||
|
updateCommand.setIdcardFront(ab98UserDto.getIdCardFront());
|
||||||
|
updateCommand.setIdcardBack(ab98UserDto.getIdCardBack());
|
||||||
|
updateCommand.setFaceImg(ab98UserDto.getFacePicture());
|
||||||
|
updateCommand.setAddress(ab98UserDto.getAddress());
|
||||||
|
updateCommand.setRegistered(true);
|
||||||
|
ab98UserApplicationService.updateUser(updateCommand);
|
||||||
|
ab98UserEntity = existingUser;
|
||||||
|
} else {
|
||||||
|
// 创建新用户
|
||||||
|
ab98UserApplicationService.addUser(addCommand);
|
||||||
|
ab98UserEntity = ab98UserService.getByIdnum(command.getIdNum());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新微信用户表的ab98UserId
|
||||||
|
WxUserEntity wxUserEntity = userService.getByOpenid(openid);
|
||||||
|
if (wxUserEntity != null) {
|
||||||
|
wxUserEntity.setAb98UserId(ab98UserEntity.getAb98UserId());
|
||||||
|
userService.updateById(wxUserEntity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.agileboot.domain.wx.user.command;
|
||||||
|
|
||||||
|
import com.agileboot.common.annotation.ExcelColumn;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加微信用户命令
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AddWxUserCommand {
|
||||||
|
|
||||||
|
@ExcelColumn(name = "openid")
|
||||||
|
private String openid;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "汇邦云用户ID")
|
||||||
|
private Long ab98UserId;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "企业用户ID")
|
||||||
|
private Long qyUserId;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "昵称")
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "手机号码")
|
||||||
|
private String tel;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "余额(分)")
|
||||||
|
private String wxBalance; // 使用 String 接收,前端传入
|
||||||
|
|
||||||
|
@ExcelColumn(name = "备注")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.agileboot.domain.wx.user.command;
|
||||||
|
|
||||||
|
import com.agileboot.common.annotation.ExcelColumn;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新微信用户命令
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UpdateWxUserCommand extends AddWxUserCommand {
|
||||||
|
|
||||||
|
@ExcelColumn(name = "主键ID")
|
||||||
|
private Long wxUserId;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.agileboot.domain.wx.user.db;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import java.util.Date;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户查询数据对象(包含关联字段)
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SearchWxUserDO {
|
||||||
|
|
||||||
|
private Long wxUserId;
|
||||||
|
|
||||||
|
private String openid;
|
||||||
|
|
||||||
|
private Long ab98UserId;
|
||||||
|
|
||||||
|
private Long qyUserId;
|
||||||
|
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
|
private String tel;
|
||||||
|
|
||||||
|
private Integer wxBalance;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.agileboot.domain.wx.user.db;
|
||||||
|
|
||||||
|
import com.agileboot.common.core.base.BaseEntity;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户信息表
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("wx_user")
|
||||||
|
@ApiModel(value = "WxUserEntity对象", description = "微信用户信息表")
|
||||||
|
public class WxUserEntity extends BaseEntity<WxUserEntity> {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("主键ID")
|
||||||
|
@TableId(value = "wx_user_id", type = IdType.AUTO)
|
||||||
|
private Long wxUserId;
|
||||||
|
|
||||||
|
@ApiModelProperty("openid")
|
||||||
|
@TableField("openid")
|
||||||
|
private String openid;
|
||||||
|
|
||||||
|
@ApiModelProperty("汇邦云用户ID")
|
||||||
|
@TableField("ab98_user_id")
|
||||||
|
private Long ab98UserId;
|
||||||
|
|
||||||
|
@ApiModelProperty("企业用户id")
|
||||||
|
@TableField("qy_user_id")
|
||||||
|
private Long qyUserId;
|
||||||
|
|
||||||
|
@ApiModelProperty("昵称")
|
||||||
|
@TableField("nick_name")
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
|
@ApiModelProperty("手机号码")
|
||||||
|
@TableField("tel")
|
||||||
|
private String tel;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户余额(单位:分)")
|
||||||
|
@TableField("wx_balance")
|
||||||
|
private Integer wxBalance;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Serializable pkVal() {
|
||||||
|
return this.wxUserId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.agileboot.domain.wx.user.db;
|
||||||
|
|
||||||
|
import com.agileboot.common.core.page.AbstractPageQuery;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户信息表 Mapper 接口
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface WxUserMapper extends BaseMapper<WxUserEntity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询微信用户列表(带关联数据)
|
||||||
|
*
|
||||||
|
* @param page 分页参数
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 微信用户列表
|
||||||
|
*/
|
||||||
|
@Select({
|
||||||
|
"<script>",
|
||||||
|
"SELECT w.*, a.nick_name as ab98_nick_name, q.nick_name as qy_nick_name",
|
||||||
|
"FROM wx_user w",
|
||||||
|
"LEFT JOIN ab98_user a ON w.ab98_user_id = a.ab98_user_id",
|
||||||
|
"LEFT JOIN qy_user q ON w.qy_user_id = q.qy_user_id",
|
||||||
|
"WHERE w.deleted = 0",
|
||||||
|
"<if test='query.openid != null and query.openid != \"\"'>",
|
||||||
|
" AND w.openid LIKE CONCAT('%', #{query.openid}, '%')",
|
||||||
|
"</if>",
|
||||||
|
"<if test='query.nickName != null and query.nickName != \"\"'>",
|
||||||
|
" AND w.nick_name LIKE CONCAT('%', #{query.nickName}, '%')",
|
||||||
|
"</if>",
|
||||||
|
"<if test='query.tel != null and query.tel != \"\"'>",
|
||||||
|
" AND w.tel LIKE CONCAT('%', #{query.tel}, '%')",
|
||||||
|
"</if>",
|
||||||
|
"ORDER BY w.create_time DESC",
|
||||||
|
"</script>"
|
||||||
|
})
|
||||||
|
Page<SearchWxUserDO> selectUserListWithJoin(
|
||||||
|
Page<SearchWxUserDO> page,
|
||||||
|
@Param("query") AbstractPageQuery<SearchWxUserDO> query
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.agileboot.domain.wx.user.db;
|
||||||
|
|
||||||
|
import com.agileboot.common.core.page.AbstractPageQuery;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户信息表 服务类
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
public interface WxUserService extends IService<WxUserEntity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测openid是否唯一
|
||||||
|
*
|
||||||
|
* @param openid openid
|
||||||
|
* @param wxUserId 微信用户ID(更新时传入,排除自身)
|
||||||
|
* @return 是否唯一
|
||||||
|
*/
|
||||||
|
boolean isOpenidUnique(String openid, Long wxUserId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测手机号是否唯一
|
||||||
|
*
|
||||||
|
* @param tel 手机号
|
||||||
|
* @param wxUserId 微信用户ID(更新时传入,排除自身)
|
||||||
|
* @return 是否唯一
|
||||||
|
*/
|
||||||
|
boolean isTelUnique(String tel, Long wxUserId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件分页查询微信用户列表
|
||||||
|
*
|
||||||
|
* @param query 查询参数
|
||||||
|
* @return 微信用户信息集合
|
||||||
|
*/
|
||||||
|
Page<WxUserEntity> getUserList(AbstractPageQuery<WxUserEntity> query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件分页查询微信用户列表(带额外字段)
|
||||||
|
*
|
||||||
|
* @param query 查询参数
|
||||||
|
* @return 微信用户信息集合(含关联字段)
|
||||||
|
*/
|
||||||
|
Page<SearchWxUserDO> getUserListWithJoin(AbstractPageQuery<SearchWxUserDO> query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据openid获取微信用户
|
||||||
|
*
|
||||||
|
* @param openid openid
|
||||||
|
* @return 微信用户信息
|
||||||
|
*/
|
||||||
|
WxUserEntity getByOpenid(String openid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户余额
|
||||||
|
*
|
||||||
|
* @param wxUserId 微信用户ID
|
||||||
|
* @param balance 余额(单位:分)
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean updateBalance(Long wxUserId, Integer balance);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package com.agileboot.domain.wx.user.db.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.agileboot.common.core.page.AbstractPageQuery;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserEntity;
|
||||||
|
import com.agileboot.domain.wx.user.db.SearchWxUserDO;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserMapper;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import java.util.Objects;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户信息表 服务实现类
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxUserServiceImpl extends ServiceImpl<WxUserMapper, WxUserEntity> implements WxUserService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOpenidUnique(String openid, Long wxUserId) {
|
||||||
|
if (StrUtil.isEmpty(openid)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<WxUserEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
|
wrapper.eq(WxUserEntity::getOpenid, openid)
|
||||||
|
.eq(WxUserEntity::getDeleted, 0);
|
||||||
|
|
||||||
|
// 如果是更新操作,排除自身
|
||||||
|
if (wxUserId != null) {
|
||||||
|
wrapper.ne(WxUserEntity::getWxUserId, wxUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(this.count(wrapper) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTelUnique(String tel, Long wxUserId) {
|
||||||
|
if (StrUtil.isEmpty(tel)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<WxUserEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
|
wrapper.eq(WxUserEntity::getTel, tel)
|
||||||
|
.eq(WxUserEntity::getDeleted, 0);
|
||||||
|
|
||||||
|
// 如果是更新操作,排除自身
|
||||||
|
if (wxUserId != null) {
|
||||||
|
wrapper.ne(WxUserEntity::getWxUserId, wxUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(this.count(wrapper) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<WxUserEntity> getUserList(AbstractPageQuery<WxUserEntity> query) {
|
||||||
|
return this.page(query.toPage(), query.addQueryCondition());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<SearchWxUserDO> getUserListWithJoin(AbstractPageQuery<SearchWxUserDO> query) {
|
||||||
|
// 自定义 SQL 查询,返回关联数据
|
||||||
|
return this.getBaseMapper().selectUserListWithJoin(query.toPage(), query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WxUserEntity getByOpenid(String openid) {
|
||||||
|
if (StrUtil.isEmpty(openid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<WxUserEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
|
wrapper.eq(WxUserEntity::getOpenid, openid)
|
||||||
|
.eq(WxUserEntity::getDeleted, 0);
|
||||||
|
|
||||||
|
return this.getOne(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateBalance(Long wxUserId, Integer balance) {
|
||||||
|
if (wxUserId == null || balance == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WxUserEntity entity = new WxUserEntity();
|
||||||
|
entity.setWxUserId(wxUserId);
|
||||||
|
entity.setWxBalance(balance);
|
||||||
|
|
||||||
|
return this.updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.agileboot.domain.wx.user.dto;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import com.agileboot.common.annotation.ExcelColumn;
|
||||||
|
import com.agileboot.common.annotation.ExcelSheet;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserEntity;
|
||||||
|
import com.agileboot.domain.wx.user.db.SearchWxUserDO;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户DTO
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@ExcelSheet(name = "微信用户列表")
|
||||||
|
@Data
|
||||||
|
public class WxUserDTO {
|
||||||
|
|
||||||
|
public WxUserDTO(WxUserEntity entity) {
|
||||||
|
if (entity != null) {
|
||||||
|
BeanUtil.copyProperties(entity, this);
|
||||||
|
|
||||||
|
// 从缓存中获取关联数据(如果需要)
|
||||||
|
// if (entity.getAb98UserId() != null) {
|
||||||
|
// this.ab98NickName = CacheCenter.ab98UserCache.get(entity.getAb98UserId() + "")...;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WxUserDTO(SearchWxUserDO entity) {
|
||||||
|
if (entity != null) {
|
||||||
|
BeanUtil.copyProperties(entity, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExcelColumn(name = "主键ID")
|
||||||
|
private Long wxUserId;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "openid")
|
||||||
|
private String openid;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "汇邦云用户ID")
|
||||||
|
private Long ab98UserId;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "企业用户ID")
|
||||||
|
private Long qyUserId;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "昵称")
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "手机号码")
|
||||||
|
private String tel;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "余额(分)")
|
||||||
|
private Integer wxBalance;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "余额(元)")
|
||||||
|
private BigDecimal wxBalanceYuan; // 转换为元显示
|
||||||
|
|
||||||
|
@ExcelColumn(name = "创建时间")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "更新时间")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
@ExcelColumn(name = "备注")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
package com.agileboot.domain.wx.user.model;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.agileboot.common.exception.ApiException;
|
||||||
|
import com.agileboot.common.exception.error.ErrorCode;
|
||||||
|
import com.agileboot.domain.wx.user.command.AddWxUserCommand;
|
||||||
|
import com.agileboot.domain.wx.user.command.UpdateWxUserCommand;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserEntity;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserService;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户领域模型
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class WxUserModel extends WxUserEntity {
|
||||||
|
|
||||||
|
private WxUserService userService;
|
||||||
|
|
||||||
|
public WxUserModel(WxUserEntity entity, WxUserService userService) {
|
||||||
|
if (entity != null) {
|
||||||
|
BeanUtil.copyProperties(entity, this);
|
||||||
|
}
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WxUserModel(WxUserService userService) {
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载添加微信用户命令
|
||||||
|
*/
|
||||||
|
public void loadAddWxUserCommand(AddWxUserCommand command) {
|
||||||
|
if (command != null) {
|
||||||
|
BeanUtil.copyProperties(command, this, "wxUserId");
|
||||||
|
|
||||||
|
// 转换余额字段
|
||||||
|
if (StrUtil.isNotEmpty(command.getWxBalance())) {
|
||||||
|
try {
|
||||||
|
// 前端传入的是元,需要转换为分
|
||||||
|
BigDecimal balanceYuan = new BigDecimal(command.getWxBalance());
|
||||||
|
this.setWxBalance(balanceYuan.multiply(new BigDecimal("100")).intValue());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "余额格式不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if (this.getWxBalance() == null) {
|
||||||
|
this.setWxBalance(0); // 默认余额0分
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载更新微信用户命令
|
||||||
|
*/
|
||||||
|
public void loadUpdateWxUserCommand(UpdateWxUserCommand command) {
|
||||||
|
if (command != null) {
|
||||||
|
loadAddWxUserCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验openid唯一性
|
||||||
|
*/
|
||||||
|
public void checkOpenidIsUnique() {
|
||||||
|
if (!userService.isOpenidUnique(getOpenid(), getWxUserId())) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "openid已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验手机号唯一性
|
||||||
|
*/
|
||||||
|
public void checkTelIsUnique() {
|
||||||
|
if (!userService.isTelUnique(getTel(), getWxUserId())) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "手机号已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验关联数据是否存在
|
||||||
|
*/
|
||||||
|
public void checkFieldRelatedEntityExist() {
|
||||||
|
// 示例:校验汇邦云用户是否存在
|
||||||
|
if (getAb98UserId() != null) {
|
||||||
|
// 可以通过其他 Service 校验
|
||||||
|
// Example: ab98UserService.getById(getAb98UserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例:校验企业用户是否存在
|
||||||
|
if (getQyUserId() != null) {
|
||||||
|
// 可以通过其他 Service 校验
|
||||||
|
// Example: qyUserService.getById(getQyUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验是否可以删除
|
||||||
|
*/
|
||||||
|
public void checkCanBeDelete() {
|
||||||
|
// 业务逻辑:例如检查用户是否有未完成的订单
|
||||||
|
// if (hasUnfinishedOrders()) {
|
||||||
|
// throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "用户存在未完成的订单,无法删除");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验余额是否合法
|
||||||
|
*/
|
||||||
|
public void checkBalanceIsValid() {
|
||||||
|
if (getWxBalance() != null && getWxBalance() < 0) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "余额不能为负数");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加余额
|
||||||
|
*
|
||||||
|
* @param amount 增加的金额(单位:分)
|
||||||
|
*/
|
||||||
|
public void increaseBalance(Integer amount) {
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "增加金额必须大于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer newBalance = (this.getWxBalance() == null ? 0 : this.getWxBalance()) + amount;
|
||||||
|
this.setWxBalance(newBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 减少余额
|
||||||
|
*
|
||||||
|
* @param amount 减少的金额(单位:分)
|
||||||
|
*/
|
||||||
|
public void decreaseBalance(Integer amount) {
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "减少金额必须大于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer currentBalance = this.getWxBalance() == null ? 0 : this.getWxBalance();
|
||||||
|
if (currentBalance < amount) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "余额不足");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setWxBalance(currentBalance - amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预保存校验
|
||||||
|
*/
|
||||||
|
public void validateBeforeSave() {
|
||||||
|
if (StrUtil.isEmpty(getOpenid())) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "openid不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StrUtil.isEmpty(getNickName())) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_BAD_REQUEST, "昵称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOpenidIsUnique();
|
||||||
|
checkTelIsUnique();
|
||||||
|
checkFieldRelatedEntityExist();
|
||||||
|
checkBalanceIsValid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.agileboot.domain.wx.user.model;
|
||||||
|
|
||||||
|
import com.agileboot.common.exception.ApiException;
|
||||||
|
import com.agileboot.common.exception.error.ErrorCode;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserEntity;
|
||||||
|
import com.agileboot.domain.wx.user.db.WxUserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户模型工厂
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxUserModelFactory {
|
||||||
|
|
||||||
|
private final WxUserService userService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID加载微信用户模型
|
||||||
|
*
|
||||||
|
* @param wxUserId 微信用户ID
|
||||||
|
* @return 微信用户模型
|
||||||
|
*/
|
||||||
|
public WxUserModel loadById(Long wxUserId) {
|
||||||
|
WxUserEntity entity = userService.getById(wxUserId);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, wxUserId, "微信用户");
|
||||||
|
}
|
||||||
|
return new WxUserModel(entity, userService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据openid加载微信用户模型
|
||||||
|
*
|
||||||
|
* @param openid openid
|
||||||
|
* @return 微信用户模型
|
||||||
|
*/
|
||||||
|
public WxUserModel loadByOpenid(String openid) {
|
||||||
|
WxUserEntity entity = userService.getByOpenid(openid);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new ApiException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, openid, "openid");
|
||||||
|
}
|
||||||
|
return new WxUserModel(entity, userService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新微信用户模型
|
||||||
|
*
|
||||||
|
* @return 微信用户模型
|
||||||
|
*/
|
||||||
|
public WxUserModel create() {
|
||||||
|
return new WxUserModel(userService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.agileboot.domain.wx.user.query;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.agileboot.common.core.page.AbstractPageQuery;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信用户查询条件
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class SearchWxUserQuery<T> extends AbstractPageQuery<T> {
|
||||||
|
|
||||||
|
private Long wxUserId;
|
||||||
|
|
||||||
|
private String openid;
|
||||||
|
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
|
private String tel;
|
||||||
|
|
||||||
|
private Long ab98UserId;
|
||||||
|
|
||||||
|
private Long qyUserId;
|
||||||
|
|
||||||
|
private Integer minBalance;
|
||||||
|
|
||||||
|
private Integer maxBalance;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QueryWrapper<T> addQueryCondition() {
|
||||||
|
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
|
||||||
|
|
||||||
|
queryWrapper.like(StrUtil.isNotEmpty(openid), "openid", openid)
|
||||||
|
.like(StrUtil.isNotEmpty(nickName), "nick_name", nickName)
|
||||||
|
.like(StrUtil.isNotEmpty(tel), "tel", tel)
|
||||||
|
.eq(wxUserId != null, "wx_user_id", wxUserId)
|
||||||
|
.eq(ab98UserId != null, "ab98_user_id", ab98UserId)
|
||||||
|
.eq(qyUserId != null, "qy_user_id", qyUserId)
|
||||||
|
.eq("deleted", 0)
|
||||||
|
.between(minBalance != null && maxBalance != null, "wx_balance", minBalance, maxBalance);
|
||||||
|
|
||||||
|
// 设置时间范围排序字段
|
||||||
|
this.timeRangeColumn = "create_time";
|
||||||
|
|
||||||
|
return queryWrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
package com.agileboot.domain.wx.utils;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态码生成工具类
|
||||||
|
* 用于生成6位数字动态码
|
||||||
|
* @author valarchie
|
||||||
|
*/
|
||||||
|
public class DynamicCodeGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态码长度
|
||||||
|
*/
|
||||||
|
private static final int CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态码有效期(分钟)
|
||||||
|
*/
|
||||||
|
private static final int CODE_VALIDITY_MINUTES = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态码缓存键前缀
|
||||||
|
*/
|
||||||
|
public static final String DYNAMIC_CODE_CACHE_PREFIX = "wx_dynamic_code_";
|
||||||
|
|
||||||
|
private static final Random RANDOM;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
// 使用SecureRandom提供更安全的随机数生成
|
||||||
|
RANDOM = SecureRandom.getInstanceStrong();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException("Failed to initialize SecureRandom", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成6位数字动态码
|
||||||
|
* @return 6位数字字符串
|
||||||
|
*/
|
||||||
|
public static String generateDynamicCode() {
|
||||||
|
StringBuilder code = new StringBuilder();
|
||||||
|
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||||
|
code.append(RANDOM.nextInt(10));
|
||||||
|
}
|
||||||
|
return code.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成指定长度的数字动态码
|
||||||
|
* @param length 动态码长度
|
||||||
|
* @return 数字字符串
|
||||||
|
*/
|
||||||
|
public static String generateDynamicCode(int length) {
|
||||||
|
if (length <= 0) {
|
||||||
|
throw new IllegalArgumentException("Length must be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder code = new StringBuilder();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
code.append(RANDOM.nextInt(10));
|
||||||
|
}
|
||||||
|
return code.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证动态码格式是否正确
|
||||||
|
* @param code 要验证的动态码
|
||||||
|
* @return 是否为6位数字
|
||||||
|
*/
|
||||||
|
public static boolean isValidDynamicCode(String code) {
|
||||||
|
if (code == null || code.length() != CODE_LENGTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否全为数字
|
||||||
|
for (char c : code.toCharArray()) {
|
||||||
|
if (!Character.isDigit(c)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取动态码默认有效期(分钟)
|
||||||
|
* @return 有效期分钟数
|
||||||
|
*/
|
||||||
|
public static int getCodeValidityMinutes() {
|
||||||
|
return CODE_VALIDITY_MINUTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取动态码长度
|
||||||
|
* @return 动态码长度
|
||||||
|
*/
|
||||||
|
public static int getCodeLength() {
|
||||||
|
return CODE_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一的6位数字动态码
|
||||||
|
* 通过检查缓存避免重复
|
||||||
|
* @param existsChecker 检查动态码是否已存在的函数,参数为动态码,返回值为缓存中存储的值
|
||||||
|
* @return 唯一的6位数字动态码
|
||||||
|
* @throws RuntimeException 当超过最大尝试次数时抛出
|
||||||
|
*/
|
||||||
|
public static String generateUniqueDynamicCode(Function<String, String> existsChecker) {
|
||||||
|
final int MAX_ATTEMPTS = 20; // 最大尝试次数
|
||||||
|
int attempts = 0;
|
||||||
|
|
||||||
|
while (attempts < MAX_ATTEMPTS) {
|
||||||
|
// 生成6位数字动态码
|
||||||
|
String dynamicCode = generateDynamicCode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查缓存中是否已存在此动态码
|
||||||
|
String result = existsChecker.apply(dynamicCode);
|
||||||
|
|
||||||
|
// 如果缓存中没有此动态码,说明是唯一的,可以使用
|
||||||
|
if (result == null) {
|
||||||
|
return dynamicCode;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 检查异常,记录日志但继续尝试
|
||||||
|
System.err.println("检查动态码是否存在时发生错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
System.err.println("动态码 " + dynamicCode + " 已存在,尝试第 " + attempts + " 次重新生成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果尝试多次后仍无法生成唯一动态码,抛出异常
|
||||||
|
throw new RuntimeException("生成唯一动态码失败,已尝试 " + MAX_ATTEMPTS + " 次");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建带前缀的动态码缓存键
|
||||||
|
* @param dynamicCode 动态码
|
||||||
|
* @return 带前缀的缓存键
|
||||||
|
*/
|
||||||
|
public static String buildCacheKey(String dynamicCode) {
|
||||||
|
return DYNAMIC_CODE_CACHE_PREFIX + dynamicCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从带前缀的缓存键中提取原始动态码
|
||||||
|
* @param cacheKey 带前缀的缓存键
|
||||||
|
* @return 原始动态码,如果键格式不正确则返回null
|
||||||
|
*/
|
||||||
|
public static String extractDynamicCode(String cacheKey) {
|
||||||
|
if (cacheKey != null && cacheKey.startsWith(DYNAMIC_CODE_CACHE_PREFIX)) {
|
||||||
|
return cacheKey.substring(DYNAMIC_CODE_CACHE_PREFIX.length());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一的6位数字动态码(带缓存键前缀检查)
|
||||||
|
* 通过检查缓存避免重复,自动使用缓存键前缀
|
||||||
|
* @param cacheProvider 提供缓存访问的函数,参数为带前缀的缓存键,返回值为缓存中存储的值
|
||||||
|
* @return 唯一的6位数字动态码
|
||||||
|
* @throws RuntimeException 当超过最大尝试次数时抛出
|
||||||
|
*/
|
||||||
|
public static String generateUniqueDynamicCodeWithPrefix(Function<String, String> cacheProvider) {
|
||||||
|
final int MAX_ATTEMPTS = 20; // 最大尝试次数
|
||||||
|
int attempts = 0;
|
||||||
|
|
||||||
|
while (attempts < MAX_ATTEMPTS) {
|
||||||
|
// 生成6位数字动态码
|
||||||
|
String dynamicCode = generateDynamicCode();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建带前缀的缓存键
|
||||||
|
String cacheKey = buildCacheKey(dynamicCode);
|
||||||
|
|
||||||
|
// 检查缓存中是否已存在此动态码
|
||||||
|
String result = cacheProvider.apply(cacheKey);
|
||||||
|
|
||||||
|
// 如果缓存中没有此动态码,说明是唯一的,可以使用
|
||||||
|
if (result == null) {
|
||||||
|
return dynamicCode;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 检查异常,记录日志但继续尝试
|
||||||
|
System.err.println("检查动态码是否存在时发生错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
System.err.println("动态码 " + dynamicCode + " 已存在,尝试第 " + attempts + " 次重新生成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果尝试多次后仍无法生成唯一动态码,抛出异常
|
||||||
|
throw new RuntimeException("生成唯一动态码失败,已尝试 " + MAX_ATTEMPTS + " 次");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.agileboot.domain.wx.utils;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信昵称生成器
|
||||||
|
* 生成格式:固定前缀_随机6位大小写字母数字
|
||||||
|
*
|
||||||
|
* @author your-name
|
||||||
|
* @since 2025-01-01
|
||||||
|
*/
|
||||||
|
public class WxNicknameGenerator {
|
||||||
|
|
||||||
|
private static final String PREFIX = "微信用户";
|
||||||
|
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
private static final int RANDOM_LENGTH = 6;
|
||||||
|
private static final SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机昵称
|
||||||
|
* 格式:微信用户_XXXXXX(其中XXXXXX为6位随机大小写字母数字)
|
||||||
|
*
|
||||||
|
* @return 生成的昵称
|
||||||
|
*/
|
||||||
|
public static String generateRandomNickname() {
|
||||||
|
StringBuilder randomPart = new StringBuilder(RANDOM_LENGTH);
|
||||||
|
|
||||||
|
for (int i = 0; i < RANDOM_LENGTH; i++) {
|
||||||
|
int index = random.nextInt(CHARACTERS.length());
|
||||||
|
randomPart.append(CHARACTERS.charAt(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PREFIX + "_" + randomPart.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成指定前缀的随机昵称
|
||||||
|
* 格式:{前缀}_XXXXXX(其中XXXXXX为6位随机大小写字母数字)
|
||||||
|
*
|
||||||
|
* @param prefix 自定义前缀
|
||||||
|
* @return 生成的昵称
|
||||||
|
*/
|
||||||
|
public static String generateRandomNickname(String prefix) {
|
||||||
|
StringBuilder randomPart = new StringBuilder(RANDOM_LENGTH);
|
||||||
|
|
||||||
|
for (int i = 0; i < RANDOM_LENGTH; i++) {
|
||||||
|
int index = random.nextInt(CHARACTERS.length());
|
||||||
|
randomPart.append(CHARACTERS.charAt(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + "_" + randomPart.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|
@ -17,28 +16,53 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractCaffeineCacheTemplate<T> {
|
public abstract class AbstractCaffeineCacheTemplate<T> {
|
||||||
|
|
||||||
private final LoadingCache<String, Optional<T>> caffeineCache = Caffeine.newBuilder()
|
private final LoadingCache<String, Optional<T>> caffeineCache;
|
||||||
// 基于容量回收。缓存的最大数量
|
|
||||||
.maximumSize(1024)
|
/**
|
||||||
// 基于容量回收。但这是统计占用内存大小,maximumWeight与maximumSize不能同时使用
|
* 默认构造函数,使用10小时的过期时间和刷新时间
|
||||||
// .maximumWeight(1000)
|
*/
|
||||||
// 设置权重(可当成每个缓存占用的大小)
|
public AbstractCaffeineCacheTemplate() {
|
||||||
// .weigher((key, value) -> 1)
|
this(10, TimeUnit.HOURS, 10, TimeUnit.HOURS);
|
||||||
// 设置软引用值
|
}
|
||||||
.softValues()
|
|
||||||
// 设置过期时间 - 最后一次写入后经过固定时间过期
|
/**
|
||||||
.expireAfterWrite(10, TimeUnit.HOURS)
|
* 构造函数,支持自定义过期时间和刷新时间
|
||||||
// 设置刷新时间 - 写入后经过固定时间刷新
|
* @param expireAfterWriteDuration 过期时间数值
|
||||||
.refreshAfterWrite(10, TimeUnit.HOURS)
|
* @param expireAfterWriteUnit 过期时间单位
|
||||||
// 所有segment的初始总容量大小
|
* @param refreshAfterWriteDuration 刷新时间数值
|
||||||
.initialCapacity(128)
|
* @param refreshAfterWriteUnit 刷新时间单位
|
||||||
// 开启缓存统计
|
*/
|
||||||
.recordStats()
|
public AbstractCaffeineCacheTemplate(long expireAfterWriteDuration, TimeUnit expireAfterWriteUnit,
|
||||||
// 移除监听器
|
long refreshAfterWriteDuration, TimeUnit refreshAfterWriteUnit) {
|
||||||
.removalListener((key, value, cause) -> {
|
Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder()
|
||||||
log.debug("触发删除动作,删除的key={}, 原因={}", key, cause);
|
// 基于容量回收。缓存的最大数量
|
||||||
})
|
.maximumSize(1024)
|
||||||
.build(new CacheLoader<String, Optional<T>>() {
|
// 基于容量回收。但这是统计占用内存大小,maximumWeight与maximumSize不能同时使用
|
||||||
|
// .maximumWeight(1000)
|
||||||
|
// 设置权重(可当成每个缓存占用的大小)
|
||||||
|
// .weigher((key, value) -> 1)
|
||||||
|
// 设置软引用值
|
||||||
|
.softValues()
|
||||||
|
// 所有segment的初始总容量大小
|
||||||
|
.initialCapacity(128)
|
||||||
|
// 开启缓存统计
|
||||||
|
.recordStats()
|
||||||
|
// 移除监听器
|
||||||
|
.removalListener((key, value, cause) -> {
|
||||||
|
log.debug("触发删除动作,删除的key={}, 原因={}", key, cause);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果过期时间大于0,则设置过期策略
|
||||||
|
if (expireAfterWriteDuration > 0) {
|
||||||
|
caffeineBuilder.expireAfterWrite(expireAfterWriteDuration, expireAfterWriteUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果刷新时间大于0,则设置刷新策略
|
||||||
|
if (refreshAfterWriteDuration > 0) {
|
||||||
|
caffeineBuilder.refreshAfterWrite(refreshAfterWriteDuration, refreshAfterWriteUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.caffeineCache = caffeineBuilder.build(new CacheLoader<String, Optional<T>>() {
|
||||||
@Override
|
@Override
|
||||||
public Optional<T> load(String key) {
|
public Optional<T> load(String key) {
|
||||||
T cacheObject = getObjectFromDb(key);
|
T cacheObject = getObjectFromDb(key);
|
||||||
|
|
@ -55,6 +79,7 @@ public abstract class AbstractCaffeineCacheTemplate<T> {
|
||||||
}, executor);
|
}, executor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从缓存中获取对象
|
* 从缓存中获取对象
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
mvn clean package -pl agileboot-admin -am
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
mvn clean package -pl agileboot-api -am
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
mvn clean package -pl agileboot-api -am
|
|
||||||
mvn clean package -pl agileboot-admin -am
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue