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