Compare commits

...

8 Commits

Author SHA1 Message Date
dzq 8a02234dcc feat(微信登录): 添加微信小程序用户绑定功能
实现微信小程序用户通过动态码、姓名和身份证绑定到汇邦云的功能。包括:
1. 在WxLoginController中添加绑定接口
2. 在WxUserApplicationService中实现绑定逻辑
3. 处理动态码验证、用户信息匹配和数据库更新
2025-11-06 17:13:49 +08:00
dzq 23153a8672 feat(微信): 添加微信用户绑定命令类并移除缓存键返回
移除动态码接口中不必要的缓存键返回字段,并新增微信用户绑定命令类用于接收绑定请求参数
2025-11-06 16:11:53 +08:00
dzq 09fce0754d feat(微信): 添加动态码生成功能及相关缓存支持
新增动态码生成工具类DynamicCodeGenerator,提供唯一6位数字动态码生成及验证功能
在CacheCenter和CaffeineCacheService中添加dynamicCodeCache支持动态码缓存
在WxController中添加生成动态码接口
新增build脚本用于构建不同模块
2025-11-06 11:14:42 +08:00
dzq 509d57596f refactor(缓存): 重构Caffeine缓存模板以支持自定义过期时间
重构AbstractCaffeineCacheTemplate使其支持自定义过期和刷新时间配置
为不同业务场景的缓存实例配置合理的过期时间
2025-11-06 10:28:05 +08:00
dzq 147194a116 feat(微信用户): 实现微信用户自动创建及信息获取功能
新增微信昵称生成器工具类,用于生成随机昵称
在WxUserApplicationService中添加getOrCreateUserByOpenid方法,实现用户不存在时自动创建
修改WxController的mpCodeToOpenId接口返回完整用户信息
新增getWxUserByOpenid接口用于获取用户信息
2025-11-06 10:11:37 +08:00
dzq 03b50542fa feat(wx用户): 新增微信用户模块功能实现
实现微信用户模块的完整功能,包括:
1. 新增用户增删改查基础功能
2. 添加用户余额管理功能
3. 实现用户关联数据查询
4. 完善参数校验和业务规则

新增错误码COMMON_BAD_REQUEST用于参数校验
2025-11-05 11:30:38 +08:00
dzq 48cab32859 docs(DDD-CQRS): 简化目录结构并更新开发指南
重构文档中的目录结构描述,从多层嵌套改为单层子文件夹原则
新增架构原则与文件组织规范章节,明确目录职责和命名规范
更新核心组件详解中的文件路径说明以符合新规范
2025-11-05 11:07:24 +08:00
dzq 46b760cef6 docs: 新增DDD/CQRS开发指南和项目文档
添加CLAUDE.md项目概述文档和DDD-CQRS开发指南文档
2025-11-05 10:50:14 +08:00
28 changed files with 3236 additions and 36 deletions

View File

@ -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": []
}
}

View File

@ -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"
} }

300
CLAUDE.md Normal file
View File

@ -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

View File

@ -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()));
}
}
} }

View File

@ -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()));
}
}
} }

View File

@ -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"),
; ;

View File

@ -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;
}

View File

@ -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;
} }
} }

View File

@ -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();
} }
} }

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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
);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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 + "");
}
}

View File

@ -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();
}
}

View File

@ -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);
} }
}); });
}
/** /**
* 从缓存中获取对象 * 从缓存中获取对象

1
build-admin.bat Normal file
View File

@ -0,0 +1 @@
mvn clean package -pl agileboot-admin -am

1
build-api.bat Normal file
View File

@ -0,0 +1 @@
mvn clean package -pl agileboot-api -am

View File

@ -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