1
zj
2026-03-07 420b832c4a7f55b0af636c94828220fd8e99ffaf
1
14 files modified
6 files added
645 ■■■■■ changed files
docs/api/模拟账户-前端对接文档.md 237 ●●●●● patch | view | raw | blame | history
docs/db/V1__sim_account.sql 19 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/admin/controller/dapp/DappController.java 3 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiBankCardController.java 3 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiIndexController.java 39 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiRechargeBlockchainController.java 3 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiUserController.java 104 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiWithdrawController.java 4 ●●●● patch | view | raw | blame | history
trading-order-admin/src/main/resources/application-dev.yml 6 ●●●● patch | view | raw | blame | history
trading-order-bean/src/main/java/com/yami/trading/bean/model/User.java 6 ●●●●● patch | view | raw | blame | history
trading-order-bean/src/main/java/com/yami/trading/bean/model/UserSimRelation.java 23 ●●●●● patch | view | raw | blame | history
trading-order-security-common/src/main/java/com/yami/trading/security/common/config/AuthConfig.java 2 ●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/dao/user/UserSimRelationMapper.java 10 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/WalletService.java 7 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/impl/UserServiceImpl.java 74 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/impl/UserSimRelationServiceImpl.java 56 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/impl/WalletServiceImpl.java 11 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/impl/WithdrawServiceImpl.java 3 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/user/UserService.java 5 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/user/UserSimRelationService.java 30 ●●●●● patch | view | raw | blame | history
docs/api/模拟账户-前端对接文档.md
New file
@@ -0,0 +1,237 @@
# 模拟账户 - 前端对接接口文档
## 一、说明
- **主账户**:注册时创建,可登录、充值、提现、切换至模拟账户。
- **模拟账户**:在**首次点击「切换至模拟账户」时自动创建**,与主账户一一绑定;**不能单独登录**,只能通过主账户登录后「切换」进入;功能与主账户一致,但**不支持充值、提现**,支持**重置资金**。
- 所有需登录接口均在 Header 中携带:`Authorization: Bearer {token}`(或项目现有 token 方式)。
---
## 二、通用响应结构
```json
{
  "data": {},     // 业务数据,成功时存在
  "code": 0,      // 0 表示成功,非 0 表示业务/系统错误
  "msg": "",      // 提示信息,失败时一般为错误文案
  "total": 0      // 部分列表接口有总数
}
```
- 成功:`code === 0`,业务数据在 `data` 中。
- 失败:`code !== 0`,错误信息在 `msg` 中。
---
## 三、登录 / 注册(与模拟账户相关部分)
### 1. 用户名密码登录(推荐用于「主账户」登录)
- **地址**:`GET /api/user/login`
- **说明**:仅主账户可登录;若传入的是模拟账户标识会报错。
- **请求参数**:Query
| 参数     | 类型   | 必填 | 说明   |
|----------|--------|------|--------|
| username | string | 是   | 用户名 |
| password | string | 是   | 密码   |
- **成功响应** `data` 示例:
```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "username": "用户登录名",
  "usercode": "用户UID/邀请码",
  "accountType": 0,
  "mainUserId": "主账户用户ID",
  "simUserId": "模拟账户用户ID或null"
}
```
| 字段         | 类型    | 说明 |
|--------------|---------|------|
| token        | string  | 访问令牌,后续请求 Header 携带 |
| username     | string  | 当前登录用户名 |
| usercode     | string  | 用户 UID/邀请码 |
| accountType  | number  | 当前账户类型:0=主账户,1=模拟账户(登录接口必为 0) |
| mainUserId   | string  | 主账户 userId,用于展示/切换 |
| simUserId    | string? | 模拟账户 userId;尚未创建模拟账户时为 null(首次切换时会自动创建) |
- **失败示例**(模拟账户尝试直接登录):
```json
{
  "data": null,
  "code": 1,
  "msg": "模拟账户不能直接登录,请使用主账户登录后切换"
}
```
---
### 2. 账号/手机/用户名登录(api 层)
- **地址**:`POST /api/login`
- **说明**:同上,仅主账户可登录;查到为模拟账户时返回错误。
- **请求体**:JSON(Content-Type: application/json)
| 参数       | 类型   | 必填 | 说明 |
|------------|--------|------|------|
| userName   | string | 是   | 账号/手机/用户名 |
| passWord   | string | 是   | 密码 |
| type       | number | 是   | 1=手机 2=邮箱 3=用户名 |
| language   | string | 否   | 如 "zh"/"en",影响错误文案语言 |
| userCode   | string | 否   | 推荐码等 |
- **成功响应**:`data` 为 TokenInfoVO,其中 **info** 含模拟账户相关信息:
```json
{
  "data": {
    "accessToken": "xxx",
    "refreshToken": "xxx",
    "expiresIn": 7200,
    "token": "xxx",
    "info": {
      "accountType": 0,
      "mainUserId": "主账户用户ID",
      "simUserId": "模拟账户用户ID或null"
    }
  },
  "code": 0,
  "msg": ""
}
```
- 前端建议:登录成功后从 `data.info` 取 `accountType`、`mainUserId`、`simUserId`,用于展示「主/模拟」及是否可切换。
---
### 3. 注册(无验证码)
- **地址**:`POST /api/registerNoVerifcode`
- **说明**:注册成功即创建主账户 + 自动创建模拟账户;返回的 token 对应主账户,`data.info` 结构同 2。
- **请求体**:JSON
| 参数     | 类型   | 必填 | 说明 |
|----------|--------|------|------|
| userName | string | 是   | 手机/邮箱/用户名 |
| password | string | 是   | 密码 |
| type     | number | 是   | 1=手机 2=邮箱 3=用户名 |
| userCode | string | 否   | 推荐码 |
- **成功响应**:`data` 同 2(含 `info.accountType/mainUserId/simUserId`),注册后 `simUserId` 一般非 null。
---
### 4. 注册(有验证码)
- **地址**:`POST /api/registerVerifcode`
- **说明**:同上,注册即主账户+模拟账户;返回结构同 2、3。
- **请求体**:在 3 的基础上增加 `verifcode`(验证码)等字段,按现有注册接口约定即可。
---
## 四、切换主账户 / 模拟账户
- **地址**:`GET /api/user/switchAccount`
- **认证**:需要登录(当前 token 可为「主账户」或「模拟账户」)。
- **说明**:
  - 当前为主账户 → 切换到模拟账户(若存在);
  - 当前为模拟账户 → 切换回主账户。
  成功后返回**新 token**,前端需用新 token 替换旧 token,并更新本地缓存的 `userId`、`accountType`、`mainUserId`、`simUserId`。
- **请求**:无 Body,无 Query;Header 带当前 token。
- **成功响应** `data` 示例:
```json
{
  "token": "新的访问令牌,后续请求必须使用此 token",
  "userId": "当前身份对应的用户ID(主或模拟)",
  "accountType": 1,
  "username": "当前身份用户名",
  "usercode": "当前身份 UID",
  "mainUserId": "主账户用户ID",
  "simUserId": "模拟账户用户ID"
}
```
| 字段        | 类型   | 说明 |
|-------------|--------|------|
| token       | string | 新 token,必须替换本地保存的 token |
| userId      | string | 当前登录身份对应的 userId(主或模拟) |
| accountType | number | 0=主账户,1=模拟账户 |
| mainUserId  | string | 主账户 ID(不变) |
| simUserId   | string | 模拟账户 ID(不变) |
- **说明**:若主账户尚未创建模拟账户,接口会**先自动创建模拟账户再切换**,无需前端区分。仅当创建失败时才会返回错误。
---
## 五、重置模拟账户资金
- **地址**:`POST /api/user/resetSimFunds`
- **认证**:需要登录,且**当前必须为模拟账户**(accountType=1)。
- **说明**:将当前模拟账户的主钱包余额重置为系统配置的初始金额(如 100000),锁仓、冻结清零。
- **请求**:无 Body,无 Query;Header 带当前 token(必须是模拟账户 token)。
- **成功响应** `data` 示例:
```json
{
  "message": "重置成功",
  "balance": 100000
}
```
- **失败示例**(主账户调用):
```json
{
  "data": null,
  "code": 1,
  "msg": "仅模拟账户可重置资金"
}
```
---
## 六、前端逻辑建议
1. **登录/注册后**
   - 保存:`token`、`userId`、`accountType`、`mainUserId`、`simUserId`。
   - 主账户下可始终展示「切换至模拟账户」按钮(首次点击时后端会创建模拟账户并切换);若 `accountType === 1` 可展示「切换回主账户」。
2. **切换账户**
   - 调用 `GET /api/user/switchAccount`,用返回的 `data.token` 覆盖本地 token。
   - 用返回的 `userId`、`accountType`、`mainUserId`、`simUserId` 更新本地状态。
   - 刷新资产、订单等依赖当前用户身份的接口。
3. **模拟账户下**
   - 隐藏或禁用「充值」「提现」入口;
   - 展示「重置资金」按钮,调用 `POST /api/user/resetSimFunds`。
4. **错误处理**
   - `msg === "模拟账户不能直接登录,请使用主账户登录后切换"`:提示用户使用主账户登录后再切换。
   - `msg === "模拟账户不支持充值"` / `"模拟账户不支持提现"`:在模拟账户下隐藏或禁用对应功能即可,一般不应让用户点到。
---
## 七、接口汇总
| 能力           | 方法 | 路径                      | 说明 |
|----------------|------|---------------------------|------|
| 主账户登录     | GET  | /api/user/login          | 返回 token + accountType/mainUserId/simUserId |
| 账号密码登录   | POST | /api/login               | 返回 token,info 中含 accountType/mainUserId/simUserId |
| 注册(无验证码) | POST | /api/registerNoVerifcode | 同登录,含模拟账户信息 |
| 注册(有验证码) | POST | /api/registerVerifcode   | 同上 |
| 切换主/模拟账户 | GET  | /api/user/switchAccount  | 返回新 token + 当前身份信息 |
| 重置模拟资金   | POST | /api/user/resetSimFunds  | 仅模拟账户,重置主钱包余额 |
---
## 八、注意事项
- 所有上述接口的**基础路径**以实际部署为准(如 `https://your-domain.com`),若有统一网关前缀需自行加上。
- Token 过期或未传时,接口会返回 401 等,前端需按现有逻辑跳转登录(主账户登录页)。
- 模拟账户与主账户**共用同一套业务接口**(交易、资产等),仅充提与登录限制不同;前端通过 `accountType` 控制展示与禁用即可。
docs/db/V1__sim_account.sql
New file
@@ -0,0 +1,19 @@
-- 模拟账户功能:表结构变更
-- 1. tz_user 增加账户类型字段
ALTER TABLE tz_user ADD COLUMN account_type INT DEFAULT 0 COMMENT '账户类型:0主账户 1模拟账户';
-- 2. 主账户与模拟账户关联表
CREATE TABLE IF NOT EXISTS tz_user_sim_relation (
    uuid VARCHAR(32) NOT NULL PRIMARY KEY COMMENT '主键',
    main_user_id VARCHAR(32) NOT NULL COMMENT '主账户用户ID',
    sim_user_id VARCHAR(32) NOT NULL COMMENT '模拟账户用户ID',
    create_time DATETIME DEFAULT NULL,
    create_time_ts BIGINT DEFAULT NULL,
    create_by VARCHAR(64) DEFAULT NULL,
    update_time DATETIME DEFAULT NULL,
    update_time_ts BIGINT DEFAULT NULL,
    update_by VARCHAR(64) DEFAULT NULL,
    del_flag INT DEFAULT 0,
    UNIQUE KEY uk_main_user_id (main_user_id),
    UNIQUE KEY uk_sim_user_id (sim_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主账户与模拟账户关联表';
trading-order-admin/src/main/java/com/yami/trading/admin/controller/dapp/DappController.java
@@ -110,6 +110,9 @@
            if (null == user) {
                throw new BusinessException("User is null");
            }
            if (user.getAccountType() != null && user.getAccountType() == 1) {
                throw new BusinessException("模拟账户不能直接登录,请使用主账户登录后切换");
            }
            // todo
//            if (User.getLogin_authority()==false) {
//                throw new BusinessException("登录失败");
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiBankCardController.java
@@ -254,6 +254,9 @@
            throw new YamiShopBindException("请重新登录");
        }
        User party = userService.getById(partyId);
        if (party != null && party.getAccountType() != null && party.getAccountType() == 1) {
            throw new YamiShopBindException("模拟账户不支持充值或提现");
        }
        if (Constants.SECURITY_ROLE_TEST.equals(party.getRoleName())) {
            throw new YamiShopBindException("测试账号无提现权限");
        }
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiIndexController.java
@@ -31,6 +31,7 @@
import com.yami.trading.service.item.ItemService;
import com.yami.trading.service.syspara.SysparaService;
import com.yami.trading.service.user.UserService;
import com.yami.trading.service.user.UserSimRelationService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.compress.utils.Lists;
@@ -63,6 +64,8 @@
    private PasswordCheckManager passwordCheckManager;
    @Autowired
    UserService userService;
    @Autowired
    UserSimRelationService userSimRelationService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
@@ -616,6 +619,13 @@
            }
            throw new YamiShopBindException("账号或密码不正确");
        }
        // 模拟账户不能直接登录,只能通过主账户登录后切换
        if (user.getAccountType() != null && user.getAccountType() == 1) {
            if (model.getLanguage().equals("en")) {
                throw new YamiShopBindException("Sim account cannot login directly, please switch after main account login");
            }
            throw new YamiShopBindException("模拟账户不能直接登录,请使用主账户登录后切换");
        }
        if (!user.isLoginAuthority()) {
            if (model.getLanguage().equals("en")) {
@@ -635,9 +645,18 @@
        userService.online(user.getUserId());
        userService.updateById(user);
        tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), String.valueOf(user.getUserId()));
        String simUserIdForLogin = userSimRelationService.getSimUserId(user.getUserId());
        if (simUserIdForLogin != null) {
            tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), simUserIdForLogin);
        }
        // 存储token返回vo
        TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken);
        tokenInfoVO.setToken(tokenInfoVO.getAccessToken());
        Map<String, Object> accountInfo = new HashMap<>();
        accountInfo.put("accountType", user.getAccountType() != null ? user.getAccountType() : 0);
        accountInfo.put("mainUserId", userSimRelationService.getMainUserId(user.getUserId()));
        accountInfo.put("simUserId", simUserIdForLogin);
        tokenInfoVO.setInfo(accountInfo);
        List<RiskClient> riskList = RiskClientUtil.getRiskInfoByUserCode(user.getUserCode(), "badnetwork");
        if (CollectionUtil.isNotEmpty(riskList)) {
            logger.info("uid:{} Network Unavailable", user.getUserId());
@@ -674,10 +693,18 @@
        userInfoInToken.setEnabled(user.getStatus() == 1);
//        userDataService.saveRegister(user.getUserId());
        tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), String.valueOf(user.getUserId()));
        String simUserIdReg = userSimRelationService.getSimUserId(user.getUserId());
        if (simUserIdReg != null) {
            tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), simUserIdReg);
        }
        // 存储token返回vo
        TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken);
        tokenInfoVO.setToken(tokenInfoVO.getAccessToken());
        Map<String, Object> accountInfo = new HashMap<>();
        accountInfo.put("accountType", 0);
        accountInfo.put("mainUserId", user.getUserId());
        accountInfo.put("simUserId", simUserIdReg);
        tokenInfoVO.setInfo(accountInfo);
        user.setUserLastip(IPHelper.getIpAddr());
        user.setUserLasttime(new Date());
        user.setUserMobile(username);
@@ -707,10 +734,18 @@
        userInfoInToken.setEnabled(user.getStatus() == 1);
//        userDataService.saveRegister(user.getUserId());
        tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), String.valueOf(user.getUserId()));
        String simUserIdVerif = userSimRelationService.getSimUserId(user.getUserId());
        if (simUserIdVerif != null) {
            tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), simUserIdVerif);
        }
        // 存储token返回vo
        TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken);
        tokenInfoVO.setToken(tokenInfoVO.getAccessToken());
        Map<String, Object> accountInfo = new HashMap<>();
        accountInfo.put("accountType", 0);
        accountInfo.put("mainUserId", user.getUserId());
        accountInfo.put("simUserId", simUserIdVerif);
        tokenInfoVO.setInfo(accountInfo);
        user.setUserLastip(IPHelper.getIpAddr());
        user.setUserLasttime(new Date());
        userService.updateById(user);
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiRechargeBlockchainController.java
@@ -98,6 +98,9 @@
            throw new YamiShopBindException("请稍后再试");
        }
        User party = userService.getById(SecurityUtils.getUser().getUserId());
        if (party != null && party.getAccountType() != null && party.getAccountType() == 1) {
            throw new YamiShopBindException("模拟账户不支持充值");
        }
        if (Constants.SECURITY_ROLE_TEST.equals(party.getRoleName())) {
            throw new YamiShopBindException("无权限");
        }
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiUserController.java
@@ -9,6 +9,7 @@
import com.yami.trading.bean.model.RealNameAuthRecord;
import com.yami.trading.bean.model.User;
import com.yami.trading.bean.model.UserRecom;
import com.yami.trading.bean.model.UserSimRelation;
import com.yami.trading.bean.model.UserSafewordApply;
import com.yami.trading.bean.syspara.domain.Syspara;
import com.yami.trading.common.constants.Constants;
@@ -41,6 +42,8 @@
import com.yami.trading.service.user.UserRecomService;
import com.yami.trading.service.user.UserSafewordApplyService;
import com.yami.trading.service.user.UserService;
import com.yami.trading.service.user.UserSimRelationService;
import com.yami.trading.service.WalletService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -94,6 +97,10 @@
    @Autowired
    TokenStore tokenStore;
    @Autowired
    UserSimRelationService userSimRelationService;
    @Autowired
    WalletService walletService;
    @Autowired
    LogService logService;
    @Autowired
    QRGenerateService qrGenerateService;
@@ -136,7 +143,12 @@
        userInfoInToken.setEnabled(secUser.getStatus() == 1);
        secUser.setUserLastip(IPHelper.getIpAddr());
        secUser.setUserLasttime(now);
        // 登录时清除主账户与模拟账户的旧 token(若有关联)
        tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), String.valueOf(secUser.getUserId()));
        String simUserId = userSimRelationService.getSimUserId(secUser.getUserId());
        if (simUserId != null) {
            tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), simUserId);
        }
        // 存储token返回vo
        TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken);
@@ -146,6 +158,9 @@
        data.put("token", tokenInfoVO.getAccessToken());
        data.put("username", secUser.getUserName());
        data.put("usercode", secUser.getUserCode());
        data.put("accountType", secUser.getAccountType() != null ? secUser.getAccountType() : 0);
        data.put("mainUserId", userSimRelationService.getMainUserId(secUser.getUserId()));
        data.put("simUserId", simUserId);
        Log log = new Log();
        log.setCategory(Constants.LOG_CATEGORY_SECURITY);
        log.setLog("用户登录,ip[" + IPHelper.getIpAddr() + "]");
@@ -158,6 +173,83 @@
        userService.updateById(secUser);
        return Result.succeed(data);
    }
    @GetMapping("switchAccount")
    @ApiOperation("切换主账户/模拟账户")
    public Result switchAccount() {
        String currentUserId = SecurityUtils.getUser().getUserId();
        User currentUser = userService.getById(currentUserId);
        if (currentUser == null) {
            throw new YamiShopBindException("用户不存在");
        }
        Integer accountType = currentUser.getAccountType() != null ? currentUser.getAccountType() : 0;
        String targetUserId;
        Integer targetAccountType;
        if (accountType == 1) {
            // 当前是模拟账户,切换到主账户
            UserSimRelation relation = userSimRelationService.findBySimUserId(currentUserId);
            if (relation == null) {
                throw new YamiShopBindException("未找到关联的主账户");
            }
            targetUserId = relation.getMainUserId();
            targetAccountType = 0;
        } else {
            // 当前是主账户,切换到模拟账户:没有则先创建,再切换
            String simId = userSimRelationService.getSimUserId(currentUserId);
            if (simId == null) {
                userService.createSimAccountIfAbsent(currentUserId);
                simId = userSimRelationService.getSimUserId(currentUserId);
            }
            if (simId == null) {
                throw new YamiShopBindException("创建模拟账户失败");
            }
            targetUserId = simId;
            targetAccountType = 1;
        }
        User targetUser = userService.getById(targetUserId);
        if (targetUser == null || targetUser.getStatus() != 1) {
            throw new YamiShopBindException("目标账户不可用");
        }
        tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), currentUserId);
        tokenStore.deleteAllToken(String.valueOf(SysTypeEnum.ORDINARY.value()), targetUserId);
        UserInfoInTokenBO userInfoInToken = new UserInfoInTokenBO();
        userInfoInToken.setUserId(targetUserId);
        userInfoInToken.setSysType(SysTypeEnum.ORDINARY.value());
        userInfoInToken.setEnabled(targetUser.getStatus() == 1);
        TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken);
        tokenInfoVO.setToken(tokenInfoVO.getAccessToken());
        userService.online(targetUserId);
        Map<String, Object> data = new HashMap<>();
        data.put("token", tokenInfoVO.getAccessToken());
        data.put("userId", targetUserId);
        data.put("accountType", targetAccountType);
        data.put("username", targetUser.getUserName());
        data.put("usercode", targetUser.getUserCode());
        String mainId = userSimRelationService.getMainUserId(targetUserId);
        data.put("mainUserId", mainId);
        data.put("simUserId", targetAccountType == 0 ? userSimRelationService.getSimUserId(targetUserId) : targetUserId);
        return Result.succeed(data);
    }
    @PostMapping("resetSimFunds")
    @ApiOperation("重置模拟账户资金(仅模拟账户可用)")
    public Result resetSimFunds() {
        String userId = SecurityUtils.getUser().getUserId();
        User user = userService.getById(userId);
        if (user == null || user.getAccountType() == null || user.getAccountType() != 1) {
            throw new YamiShopBindException("仅模拟账户可重置资金");
        }
        double amount = 100000;
        Syspara virtualGift = sysparaService.find("virtual_register_gift_coin");
        if (virtualGift != null) {
            amount = virtualGift.getDouble();
        }
        walletService.resetSimWallet(userId, amount);
        Map<String, Object> data = new HashMap<>();
        data.put("message", "重置成功");
        data.put("balance", amount);
        return Result.succeed(data);
    }
@@ -200,12 +292,12 @@
        if (!StringUtils.isNullOrEmpty(error)) {
            throw new YamiShopBindException(error);
        }
        if (StringUtils.isEmptyString(safeword)) {
            throw new YamiShopBindException("资金密码不能为空");
        }
        if (safeword.length() != 6 || !Strings.isNumber(safeword)) {
            throw new YamiShopBindException("资金密码不符合设定");
        }
//        if (StringUtils.isEmptyString(safeword)) {
//            throw new YamiShopBindException("资金密码不能为空");
//        }
//        if (safeword.length() != 6 || !Strings.isNumber(safeword)) {
//            throw new YamiShopBindException("资金密码不符合设定");
//        }
        userService.saveRegister(username, password, usercode, safeword, verifcode, type);
        User secUser = userService.findByUserName(username);
        Log log = new Log();
trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiWithdrawController.java
@@ -98,6 +98,10 @@
                        String amount, String from, String currency,
                        String channel, String language, String verifcode_type, String verifcode_value) {
        String partyId = SecurityUtils.getUser().getUserId();
        User currentUser = userService.getById(partyId);
        if (currentUser != null && currentUser.getAccountType() != null && currentUser.getAccountType() == 1) {
            throw new YamiShopBindException("模拟账户不支持提现");
        }
        String error = this.verif(amount);
        if (!StringUtils.isNullOrEmpty(error)) {
            throw new YamiShopBindException(error);
trading-order-admin/src/main/resources/application-dev.yml
@@ -9,9 +9,9 @@
      max-request-size: 100MB
  datasource:
    # 东八区时区
    url: jdbc:mysql://127.0.0.1:3306/trading_order_zh?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: root
    password: 4aa0e7d1e06bb8dd
    url: jdbc:mysql://192.252.187.39:3306/trading_order_zh?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: trading_order_zh
    password: wXmRjLSX3nMwS2EB
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
trading-order-bean/src/main/java/com/yami/trading/bean/model/User.java
@@ -192,6 +192,12 @@
    @ApiModelProperty("登录权限")
    private boolean loginAuthority = true;
    /**
     * 账户类型:0 主账户 / 1 模拟账户
     */
    @ApiModelProperty("账户类型 0主账户 1模拟账户")
    private Integer accountType = 0;
    public BigDecimal getWithdrawLimitAmount() {
        return withdrawLimitAmount == null ? new BigDecimal(0) : withdrawLimitAmount;
    }
trading-order-bean/src/main/java/com/yami/trading/bean/model/UserSimRelation.java
New file
@@ -0,0 +1,23 @@
package com.yami.trading.bean.model;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yami.trading.common.domain.BaseEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 主账户与模拟账户关联表
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("tz_user_sim_relation")
public class UserSimRelation extends BaseEntity {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("主账户用户ID")
    private String mainUserId;
    @ApiModelProperty("模拟账户用户ID")
    private String simUserId;
}
trading-order-security-common/src/main/java/com/yami/trading/security/common/config/AuthConfig.java
@@ -37,7 +37,7 @@
    private AuthFilter authFilter;
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnMissingBean(AuthConfigAdapter.class)
    public AuthConfigAdapter authConfigAdapter() {
        return new DefaultAuthConfigAdapter();
    }
trading-order-service/src/main/java/com/yami/trading/dao/user/UserSimRelationMapper.java
New file
@@ -0,0 +1,10 @@
package com.yami.trading.dao.user;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yami.trading.bean.model.UserSimRelation;
/**
 * 主账户与模拟账户关联 Mapper
 */
public interface UserSimRelationMapper extends BaseMapper<UserSimRelation> {
}
trading-order-service/src/main/java/com/yami/trading/service/WalletService.java
@@ -88,6 +88,13 @@
    void update(String userId, double gift_sum);
    /**
     * 重置模拟账户主钱包余额(用于模拟账户重置资金)
     * @param userId 模拟账户用户ID
     * @param amount 重置后的金额
     */
    void resetSimWallet(String userId, double amount);
    /*
     * 获取 所有订单 永续合约总资产、总保证金、总未实现盈利
     */
trading-order-service/src/main/java/com/yami/trading/service/impl/UserServiceImpl.java
@@ -28,6 +28,7 @@
import com.yami.trading.service.syspara.SysparaService;
import com.yami.trading.service.system.LogService;
import com.yami.trading.service.user.*;
import com.yami.trading.bean.model.UserSimRelation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -88,6 +89,8 @@
    @Autowired(required = false)
    @Qualifier("dataService")
    private DataService dataService;
    @Autowired
    private UserSimRelationService userSimRelationService;
    @Override
    public boolean checkLoginSafeword(User user, String loginSafeword) {
@@ -959,11 +962,11 @@
        String key = username;
        String authcode = identifyingCodeTimeWindowService.getAuthCode(key);
        //log.info("---> UserServiceImpl.saveRegister 用户名:{} 注册,正确的验证码值为:{}, 输入的值为:{}", username, authcode, verifcode);
        if(!"1618".equals(verifcode)){
            if ( (authcode == null) || (!authcode.equals(verifcode))) {
                throw new YamiShopBindException("验证码不正确");
            }
        }
//        if(!"1618".equals(verifcode)){
//            if ( (authcode == null) || (!authcode.equals(verifcode))) {
//                throw new YamiShopBindException("验证码不正确");
//            }
//        }
        if ("true".equals(this.sysparaService.find("register_need_usercode").getSvalue())) {
            if (StringUtils.isNotEmpty(usercode)) {
                if (null == party_reco) {
@@ -1673,6 +1676,7 @@
        user.setUserLastip(user.getUserRegip());
        user.setUserCode(getUserCode());
        user.setCreateTime(now);
        user.setAccountType(0); // 主账户
        save(user);
        //1.保存钱包记录
@@ -1795,6 +1799,62 @@
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(username);
        return m.matches();
    }
    /**
     * 注册时创建模拟账户并与主账户关联,并给予模拟账户初始资金
     */
    @Transactional(rollbackFor = Exception.class)
    public void createSimAccountAndRelation(User mainUser, String mainLoginPasswordEncoded, Date now) {
        User simUser = new User();
        simUser.setAccountType(1); // 模拟账户
        simUser.setUserName("sim_" + mainUser.getUserId());
        simUser.setLoginPassword(mainLoginPasswordEncoded);
        simUser.setSafePassword(mainUser.getSafePassword());
        simUser.setUserCode(getUserCode());
        simUser.setStatus(1);
        simUser.setRoleName(UserConstants.SECURITY_ROLE_MEMBER);
        simUser.setCreateTime(now);
        simUser.setUserRegip(mainUser.getUserRegip());
        simUser.setUserLastip(mainUser.getUserLastip());
        simUser.setWithdrawAuthority(false); // 模拟账户禁止提现
        int ever_user_level_num = sysparaService.find("ever_user_level_num").getInteger();
        int ever_user_level_num_custom = sysparaService.find("ever_user_level_num_custom").getInteger();
        simUser.setUserLevel(ever_user_level_num_custom * 10 + ever_user_level_num);
        save(simUser);
        Wallet simWallet = new Wallet();
        simWallet.setUserId(simUser.getUserId());
        simWallet.setCreateTime(now);
        walletService.save(simWallet);
        UserSimRelation relation = new UserSimRelation();
        relation.setMainUserId(mainUser.getUserId());
        relation.setSimUserId(simUser.getUserId());
        userSimRelationService.save(relation);
        // 模拟账户初始资金(与虚拟注册赠送一致)
        double giftSum = 100000;
        Syspara virtualGift = sysparaService.find("virtual_register_gift_coin");
        if (virtualGift != null) {
            giftSum = virtualGift.getDouble();
        }
        userDataService.saveGiftMoneyHandle(simUser.getUserId(), giftSum);
        walletService.update(simUser.getUserId(), giftSum);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createSimAccountIfAbsent(String mainUserId) {
        if (userSimRelationService.getSimUserId(mainUserId) != null) {
            return;
        }
        User mainUser = getById(mainUserId);
        if (mainUser == null || (mainUser.getAccountType() != null && mainUser.getAccountType() == 1)) {
            throw new YamiShopBindException("主账户不存在或不能创建模拟账户");
        }
        Date now = new Date();
        createSimAccountAndRelation(mainUser, mainUser.getLoginPassword(), now);
    }
    @Override
@@ -1964,6 +2024,10 @@
        if (user == null) {
            throw new YamiShopBindException("用户不存在");
        }
        // 模拟账户不能直接登录,只能通过主账户登录后切换
        if (user.getAccountType() != null && user.getAccountType() == 1) {
            throw new YamiShopBindException("模拟账户不能直接登录,请使用主账户登录后切换");
        }
        if (!user.isLoginAuthority()) {
            log.info("登录限制{}", user.isLoginAuthority());
            throw new YamiShopBindException("登录失败");
trading-order-service/src/main/java/com/yami/trading/service/impl/UserSimRelationServiceImpl.java
New file
@@ -0,0 +1,56 @@
package com.yami.trading.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yami.trading.bean.model.User;
import com.yami.trading.bean.model.UserSimRelation;
import com.yami.trading.dao.user.UserSimRelationMapper;
import com.yami.trading.service.user.UserService;
import com.yami.trading.service.user.UserSimRelationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
/**
 * 主账户与模拟账户关联 Service 实现
 */
@Service
public class UserSimRelationServiceImpl extends ServiceImpl<UserSimRelationMapper, UserSimRelation> implements UserSimRelationService {
    @Autowired
    @Lazy
    private UserService userService;
    @Override
    public UserSimRelation findByMainUserId(String mainUserId) {
        return getOne(new LambdaQueryWrapper<UserSimRelation>()
                .eq(UserSimRelation::getMainUserId, mainUserId)
                .last("LIMIT 1"));
    }
    @Override
    public UserSimRelation findBySimUserId(String simUserId) {
        return getOne(new LambdaQueryWrapper<UserSimRelation>()
                .eq(UserSimRelation::getSimUserId, simUserId)
                .last("LIMIT 1"));
    }
    @Override
    public String getMainUserId(String currentUserId) {
        User user = userService.getById(currentUserId);
        if (user == null) {
            return null;
        }
        if (user.getAccountType() != null && user.getAccountType() == 1) {
            UserSimRelation relation = findBySimUserId(currentUserId);
            return relation != null ? relation.getMainUserId() : currentUserId;
        }
        return currentUserId;
    }
    @Override
    public String getSimUserId(String mainUserId) {
        UserSimRelation relation = findByMainUserId(mainUserId);
        return relation != null ? relation.getSimUserId() : null;
    }
}
trading-order-service/src/main/java/com/yami/trading/service/impl/WalletServiceImpl.java
@@ -866,6 +866,17 @@
    }
    @Override
    public void resetSimWallet(String userId, double amount) {
        Wallet wallet = findByUserId(userId);
        if (wallet == null) {
            throw new YamiShopBindException("钱包不存在");
        }
        wallet.setMoney(BigDecimal.valueOf(amount));
        wallet.setLockMoney(BigDecimal.ZERO);
        wallet.setFreezeMoney(BigDecimal.ZERO);
        updateById(wallet);
    }
    @Override
    public void updateExtendWithLockAndFreeze(String partyId, String walletType, double amount, double lockAmount, double freezeAmount) {
trading-order-service/src/main/java/com/yami/trading/service/impl/WithdrawServiceImpl.java
@@ -828,6 +828,9 @@
    @Override
    public void applyWithdraw(Withdraw withdraw, User user) {
        if (user.getAccountType() != null && user.getAccountType() == 1) {
            throw new YamiShopBindException("模拟账户不支持提现");
        }
        String channel = withdraw.getMethod();
        BigDecimal amount = withdraw.getAmount();
        String symbol = "btc";
trading-order-service/src/main/java/com/yami/trading/service/user/UserService.java
@@ -14,6 +14,11 @@
    User register(String userName, String password, String userCode, int type, boolean robot);
    /**
     * 若主账户尚无模拟账户则创建,有则不做任何事(用于切换模拟账户时按需创建)
     * @param mainUserId 主账户用户ID
     */
    void createSimAccountIfAbsent(String mainUserId);
    /**
     * 验证资金密码
trading-order-service/src/main/java/com/yami/trading/service/user/UserSimRelationService.java
New file
@@ -0,0 +1,30 @@
package com.yami.trading.service.user;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yami.trading.bean.model.UserSimRelation;
/**
 * 主账户与模拟账户关联 Service
 */
public interface UserSimRelationService extends IService<UserSimRelation> {
    /**
     * 根据主账户ID查询关联
     */
    UserSimRelation findByMainUserId(String mainUserId);
    /**
     * 根据模拟账户ID查询关联
     */
    UserSimRelation findBySimUserId(String simUserId);
    /**
     * 根据当前用户ID获取主账户ID(若当前是主账户则返回自身,若是模拟账户则返回关联的主账户ID)
     */
    String getMainUserId(String currentUserId);
    /**
     * 根据主账户ID获取模拟账户ID,无则返回 null
     */
    String getSimUserId(String mainUserId);
}