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.exception.ApiException;
|
||||
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.user.WxUserApplicationService;
|
||||
import com.agileboot.domain.wx.user.dto.WxUserDTO;
|
||||
import com.agileboot.domain.wx.utils.DynamicCodeGenerator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.client.RestClientException;
|
||||
|
||||
|
|
@ -25,6 +31,8 @@ import java.util.Map;
|
|||
public class WxController {
|
||||
private final WxService wxService;
|
||||
|
||||
private final WxUserApplicationService wxUserApplicationService;
|
||||
|
||||
/**
|
||||
* 获取小程序用户OpenID
|
||||
* @param code 微信授权码
|
||||
|
|
@ -32,14 +40,83 @@ public class WxController {
|
|||
* @throws ApiException 当code无效或微信接口调用失败时抛出
|
||||
*/
|
||||
@GetMapping("/mpCodeToOpenId")
|
||||
public ResponseDTO<String> mpCodeToOpenId(String code) {
|
||||
public ResponseDTO<WxUserDTO> mpCodeToOpenId(String code) {
|
||||
try {
|
||||
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) {
|
||||
log.error("获取openid失败", e);
|
||||
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;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.agileboot.common.core.dto.ResponseDTO;
|
||||
import com.agileboot.common.exception.ApiException;
|
||||
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.user.Ab98UserApplicationService;
|
||||
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.qywx.user.QyUserApplicationService;
|
||||
import com.agileboot.domain.wx.user.WxUserApplicationService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.agileboot.domain.ab98.api.Ab98ApiUtil;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
|
|
@ -29,6 +31,7 @@ public class WxLoginController {
|
|||
private final QyUserApplicationService qyUserApplicationService;
|
||||
private final Ab98UserApplicationService ab98UserApplicationService;
|
||||
private final Ab98UserApplicationService userApplicationService;
|
||||
private final WxUserApplicationService wxUserApplicationService;
|
||||
|
||||
@PostMapping("/logout")
|
||||
@ApiOperation(value = "用户退出登录")
|
||||
|
|
@ -166,4 +169,25 @@ public class WxLoginController {
|
|||
Ab98UserEntity ab98User = userApplicationService.bindQyUser(command);
|
||||
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"),
|
||||
|
||||
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> dynamicCodeCache;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
|
||||
|
|
@ -50,6 +52,7 @@ public class CacheCenter {
|
|||
roleCache = caffeineCache.roleCache;
|
||||
postCache = caffeineCache.postCache;
|
||||
qyUseridCache = caffeineCache.qyUseridCache;
|
||||
dynamicCodeCache = caffeineCache.dynamicCodeCache;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ import org.springframework.stereotype.Component;
|
|||
@RequiredArgsConstructor
|
||||
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
|
||||
public String getObjectFromDb(Object id) {
|
||||
// 验证码通常不需要从数据库获取,这里返回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
|
||||
public SystemLoginUser getObjectFromDb(Object id) {
|
||||
// 登录用户信息通常不需要从数据库获取,这里返回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
|
||||
public SysUserEntity getObjectFromDb(Object id) {
|
||||
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
|
||||
public SysRoleEntity getObjectFromDb(Object id) {
|
||||
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
|
||||
public SysPostEntity getObjectFromDb(Object id) {
|
||||
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
|
||||
public String getObjectFromDb(Object id) {
|
||||
// 企业微信用户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 统计信息字符串
|
||||
|
|
@ -82,6 +111,7 @@ public class CaffeineCacheService {
|
|||
stats.append("Role Cache: ").append(roleCache.getStats()).append("\n");
|
||||
stats.append("Post Cache: ").append(postCache.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
|
@ -17,28 +16,53 @@ import lombok.extern.slf4j.Slf4j;
|
|||
@Slf4j
|
||||
public abstract class AbstractCaffeineCacheTemplate<T> {
|
||||
|
||||
private final LoadingCache<String, Optional<T>> caffeineCache = Caffeine.newBuilder()
|
||||
// 基于容量回收。缓存的最大数量
|
||||
.maximumSize(1024)
|
||||
// 基于容量回收。但这是统计占用内存大小,maximumWeight与maximumSize不能同时使用
|
||||
// .maximumWeight(1000)
|
||||
// 设置权重(可当成每个缓存占用的大小)
|
||||
// .weigher((key, value) -> 1)
|
||||
// 设置软引用值
|
||||
.softValues()
|
||||
// 设置过期时间 - 最后一次写入后经过固定时间过期
|
||||
.expireAfterWrite(10, TimeUnit.HOURS)
|
||||
// 设置刷新时间 - 写入后经过固定时间刷新
|
||||
.refreshAfterWrite(10, TimeUnit.HOURS)
|
||||
// 所有segment的初始总容量大小
|
||||
.initialCapacity(128)
|
||||
// 开启缓存统计
|
||||
.recordStats()
|
||||
// 移除监听器
|
||||
.removalListener((key, value, cause) -> {
|
||||
log.debug("触发删除动作,删除的key={}, 原因={}", key, cause);
|
||||
})
|
||||
.build(new CacheLoader<String, Optional<T>>() {
|
||||
private final LoadingCache<String, Optional<T>> caffeineCache;
|
||||
|
||||
/**
|
||||
* 默认构造函数,使用10小时的过期时间和刷新时间
|
||||
*/
|
||||
public AbstractCaffeineCacheTemplate() {
|
||||
this(10, TimeUnit.HOURS, 10, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数,支持自定义过期时间和刷新时间
|
||||
* @param expireAfterWriteDuration 过期时间数值
|
||||
* @param expireAfterWriteUnit 过期时间单位
|
||||
* @param refreshAfterWriteDuration 刷新时间数值
|
||||
* @param refreshAfterWriteUnit 刷新时间单位
|
||||
*/
|
||||
public AbstractCaffeineCacheTemplate(long expireAfterWriteDuration, TimeUnit expireAfterWriteUnit,
|
||||
long refreshAfterWriteDuration, TimeUnit refreshAfterWriteUnit) {
|
||||
Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder()
|
||||
// 基于容量回收。缓存的最大数量
|
||||
.maximumSize(1024)
|
||||
// 基于容量回收。但这是统计占用内存大小,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
|
||||
public Optional<T> load(String key) {
|
||||
T cacheObject = getObjectFromDb(key);
|
||||
|
|
@ -55,6 +79,7 @@ public abstract class AbstractCaffeineCacheTemplate<T> {
|
|||
}, 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