feat(微信登录): 新增微信登录相关接口及文档

添加微信登录控制器,实现获取微信登录二维码、临时token、短信验证码发送与验证、用户退出登录等功能。同时新增相关接口文档,详细描述各接口的请求与响应格式。
This commit is contained in:
dzq 2025-04-11 11:01:05 +08:00
parent 7e06c9f73f
commit e18463279b
3 changed files with 560 additions and 0 deletions

View File

@ -0,0 +1,94 @@
package com.agileboot.api.controller;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.exception.ApiException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import com.agileboot.domain.ab98.api.Ab98ApiUtil;
import javax.validation.constraints.NotBlank;
import java.util.Map;
@RestController
@RequestMapping("/api/wx/login")
@CrossOrigin(origins = "*", allowedHeaders = "*")
@RequiredArgsConstructor
@Api(tags = "微信登录接口")
public class WxLoginController {
@PostMapping("/logout")
@ApiOperation(value = "用户退出登录")
public ResponseDTO<Ab98ApiUtil.LogoutResponse> logout(@RequestParam @NotBlank String token) {
try {
return ResponseDTO.ok(Ab98ApiUtil.doLogout(token));
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 获取微信登录二维码链接
*/
@GetMapping("/wechat/qrcode")
public ResponseDTO<String> getWechatQrCode(@RequestParam @NotBlank String token) {
try {
return ResponseDTO.ok(Ab98ApiUtil.generateWechatLoginUrl(token));
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 获取临时token
*/
@GetMapping("/getToken")
@ApiOperation(value = "获取临时令牌", notes = "用于后续登录流程")
public ResponseDTO<Ab98ApiUtil.TokenResponse> getToken(@RequestParam String appName) {
try {
return ResponseDTO.ok(Ab98ApiUtil.getToken(appName));
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 发送短信验证码
*/
@PostMapping("/sendSms")
public ResponseDTO<Ab98ApiUtil.SmsSendResponse> sendSms(
@RequestParam String token,
@RequestParam String tel) {
try {
return ResponseDTO.ok(Ab98ApiUtil.sendLoginSms(token, tel));
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
/**
* 验证短信验证码
*/
@PostMapping("/verifySms")
public ResponseDTO<Ab98ApiUtil.LoginData> verifySms(
@RequestParam String token,
@RequestParam String tel,
@RequestParam String vcode) {
try {
Ab98ApiUtil.LoginResponse loginResponse = Ab98ApiUtil.verifySmsCode(token, tel, vcode);
Ab98ApiUtil.LoginData data = new Ab98ApiUtil.LoginData();
data.setFace_img(loginResponse.getOutputData().getFace_img());
data.setSuccess(loginResponse.getOutputData().isSuccess());
data.setSex(loginResponse.getOutputData().getSex());
data.setName(loginResponse.getOutputData().getName());
data.setUserid(loginResponse.getOutputData().getUserid());
data.setRegistered(loginResponse.getOutputData().isRegistered());
data.setTel(loginResponse.getOutputData().getTel());
return ResponseDTO.ok(data);
} catch (ApiException e) {
return ResponseDTO.fail(e);
}
}
}

View File

@ -0,0 +1,234 @@
package com.agileboot.domain.ab98.api;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class Ab98ApiUtil {
private static final String BASE_URL = "https://www.ab98.cn/api/doInterface";
private static final String WEBSOCKET_URL = "wss://www.ab98.cn/login.ws/";
/**
* 短信登录发送验证码
*/
/**
* 发送短信验证码短信登录
* @param token 通过getToken获取的临时令牌有效期5分钟
* @param tel 接收验证码的手机号码需符合格式11位数字
* @param nobind "true"表示不绑定手机号固定值
* @param for_login "true"表示用于登录固定值
* @param from 来源渠道固定值"jt"
*/
public static SmsSendResponse sendLoginSms(String token, String tel) {
String url = BASE_URL + "?code=doSendSms";
Map<String, Object> paramMap = new HashMap<String, Object>() {{
put("token", token);
put("tel", tel);
put("nobind", "true");
put("for_login", "true");
put("from", "jt");
}};
String response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(paramMap))
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("短信发送响应: {}", response);
SmsSendResponse resp = JSONUtil.toBean(response, SmsSendResponse.class);
checkApiResponse(resp);
return resp;
}
/**
* 验证短信验证码
*/
/**
* 验证短信验证码短信登录
* @param token 通过getToken获取的临时令牌
* @param tel 接收验证码的手机号码
* @param vcode 用户输入的6位验证码有效期5分钟
* @return 登录结果包含用户身份信息
* @throws ApiException 当验证失败或接口返回异常时抛出
*/
public static LoginResponse verifySmsCode(String token, String tel, String vcode) {
String url = BASE_URL + "?code=doCheckSmsCode";
Map<String, Object> paramMap = new HashMap<String, Object>() {{
put("token", token);
put("tel", tel);
put("vcode", vcode);
}};
String response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(paramMap))
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("短信验证响应: {}", response);
LoginResponse resp = JSONUtil.toBean(response, LoginResponse.class);
checkApiResponse(resp);
return handleLoginResult(resp);
}
/**
* 处理登录结果
*/
private static LoginResponse handleLoginResult(LoginResponse response) {
if (response.getOutputData() == null || !response.getOutputData().isSuccess()) {
log.error("登录失败: {}", response);
throw new ApiException(ErrorCode.FAILED, "登录验证失败");
}
return response;
}
/**
* 生成微信扫码登录URL
*/
public static String generateWechatLoginUrl(String token) {
return "https://www.ab98.cn/online/index.html?content=doLogin%60" + token;
}
/**
* 监听扫码登录websocket
*/
/*public static void listenLoginWebsocket(String token, WebSocketListener listener) {
String url = WEBSOCKET_URL + token;
HttpUtil.createWebSocket(url, listener).connect();
}*/
/**
* 获取登录token
*/
/**
* 获取登录token
* @param appName 应用英文名需在汇邦数字平台登记
* @return outputData.token 临时令牌后续接口需携带
* @return outputData.takeFace 是否需要人脸验证true需要刷脸
*/
public static TokenResponse getToken(String appName) {
// String url = BASE_URL + "?code=doGetToken&from=jt&app=" + appName;
String url = BASE_URL + "?code=doGetToken&from=jt";
String response = HttpUtil.createGet(url)
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("获取token响应: {}", response);
TokenResponse tokenResponse = JSONUtil.toBean(response, TokenResponse.class);
checkApiResponse(tokenResponse);
return tokenResponse;
}
/**
* 构建带参数的请求URL
*/
/**
* 检查接口响应状态
*/
private static void checkApiResponse(BaseResponse response) {
if (response.getStateCode() != 200 || !"ok".equals(response.getState())) {
log.error("接口调用失败: {}", response);
throw new ApiException(ErrorCode.FAILED, "第三方接口调用失败");
}
}
// 基础响应对象
@Data
public static class BaseResponse {
private String state;
private Integer stateCode;
private Object outputData;
}
@Data
public static class SmsSendResponse extends BaseResponse {
private SmsResult outputData;
}
@Data
public static class SmsResult {
private boolean success;
private String errMsg;
}
@Data
public static class LoginResponse extends BaseResponse {
private LoginData outputData;
}
@Data
public static class LoginData {
private String idcard_back; // 身份证背面照片地址
private String face_img; // 人脸照片地址
private String address; // 身份证登记地址
private String nation; // 民族接口文档显示可能为null
private boolean success; // 验证是否成功
private String sex; // 性别/
private String name; // 真实姓名
private String userid; // 用户在平台的唯一ID
private boolean registered; // 是否已注册
private String tel; // 手机号码
private String idnum; // 身份证号码
private String idcard_front; // 身份证正面照片地址
}
/**
* 用户退出接口
*/
public static LogoutResponse doLogout(String token) {
String url = BASE_URL + "?code=doLogout";
Map<String, Object> paramMap = new HashMap<String, Object>() {{
put("token", token);
}};
String response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(paramMap))
.header("noSign", "true")
.header("source", "api")
.execute().body();
log.info("用户退出响应: {}", response);
LogoutResponse resp = JSONUtil.toBean(response, LogoutResponse.class);
checkApiResponse(resp);
return resp;
}
@Data
public static class LogoutResponse extends BaseResponse {
private LogoutData outputData; // 包含success和logoutTime登出时间
}
@Data
public static class LogoutData {
private boolean success;
private String logoutTime;
}
// Token响应对象
@Data
public static class TokenResponse extends BaseResponse {
private TokenData outputData;
}
@Data
public static class TokenData {
private String token;
private boolean takeFace;
}
}

View File

@ -0,0 +1,232 @@
## 请求概述
### 请求header
所有请求必须包含以下两个header
* `noSign`: `true`
* `source`: `api`
获取登录token后也可以在header中添加
* `token`: `${token}`
### 登录校验结果
接口返回结果为json格式如果包含`code`字段,则说明登录校验失败。
* `code`: `0001` - 登录状态已失效
* `code`: `0002` - 在cookie、Get参数、Header中均未检测到token
## 登录接口
### 2.1 获取token
* **API**: `https://www.ab98.cn/api/doInterface?code=doGetToken&from=jt&app=${appname}`
* **请求方式**: GET
* **请求参数**:
* `code`: `doGetToken` (必传)
* `from`: `jt` (可选)
* `app`: 调用该接口的应用在汇邦数字平台登记的应用英文名 (可选)
**成功返回**:
```json
{
"outputData": {
"token": "1a6ea39e84c406283839856640e3aa66",
"takeFace": true
},
"state": "ok"
}
```
**失败返回**:
```json
{
"outputData": "错误信息",
"stateCode": 0,
"state": "fail"
}
```
### 2.2 短信登录
#### 2.2.1 发送短信
* **API**: `https://www.ab98.cn/api/doInterface?code=doSendSms`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3", //必传
"tel":"137xxxxxxxx", //必传
"nobind":"true", //必传
"for_login":"true", //必传
"from":"jt" //必传
}
```
**成功返回**:
```json
{
"outputData": {
"success": true
},
"state": "ok"
}
```
**失败返回**:
```json
{
"outputData": {
"success": false,
"errMsg": "失败原因"
},
"state": "ok"
}
```
#### 2.2.2 验证短信验证码
* **API**: `https://www.ab98.cn/api/doInterface?code=doCheckSmsCode`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3",
"tel":"137xxxxxxxx",
"vcode":"123456"
}
```
**成功返回**:
```json
{
"outputData": {
"idcard_back": "https://www.ab98.cn/upload/temp/images/202102/xxx.jpeg",
"face_img": "https://www.ab98.cn/upload/temp/images/202102/yyy.jpeg",
"address": "身份证上的地址",
"nation": null,
"success": true,
"sex": "男",
"name": "xxx",
"userid": 123,
"registered": true,
"tel": "137xxxxxxxx",
"idnum": "450981xxxxxxxxxxxx",
"idcard_front": "https://www.ab98.cn/upload/temp/images/202102/zzz.jpeg"
},
"state": "ok"
}
```
**失败返回**:
```json
{
"outputData": {
"success": false
},
"state": "ok"
}
```
### 2.3 微信扫码登录
* **API**: `https://www.ab98.cn/online/index.html?content=doLogin%60`
#### 2.3.1 二维码
假设获取到的token为`358900e1005c33a1dd059b07042ceec3`,则构造的最终链接为:
`https://www.ab98.cn/online/index.html?content=doLogin%60358900e1005c33a1dd059b07042ceec3`
使用该链接生成二维码图片供用户使用微信“扫一扫”。
#### 2.3.2 监听websocket
* **监听地址**: `wss://www.ab98.cn/login.ws/${token}`
* **登录失败**:
```json
{
"outputData": "登录失败原因",
"state": "fail"
}
```
* **登录成功**:
```json
{
"outputData": "登录成功",
"state": "ok",
"username": "扫码者姓名",
"sex": "扫码者性别",
"head_img": "扫码者头像",
"idnum": "身份证号码",
"userid": "人员记录ID",
"tel": "手机号码"
}
```
### 2.4 刷脸登录
* **API**: `https://www.ab98.cn/api/doInterface?code=doStrongFaceLogin`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3", //必传
"check_code":"1234", //必传
"imgBase64":"/9j/4AAQS…" //必传
}
```
**成功返回**:
```json
{
"state": "ok",
"outputData": "登录成功",
"openid": "oGfFD1jBEZ2sq4PhOc8zKKejHA9E",
"head_img": "https://www.ab98.cn/… ",
"sex": "男",
"userid": 1,
"token": "dbe11ef64cd6350c5d935d531db765c0",
"username": "xxx",
"tel": "xxx"
}
```
### 2.5 动态码登录
1. **发起websocket监听**:
`wss://www.ab98.cn/login.ws/{监听token}`
其中 `{监听token}` 由客户端自定义,由数字和英文字母组成,保证多用户同时监听时 `{监听token}` 互不相同即可,否则会接收到错误消息。
2. **主动发送消息**:
```json
{
"type": "mfa",
"do": "checkCode",
"token": "xxx",
"code": "000000"
}
```
**服务器响应消息**:
1. **登录失败**:
```json
{
"state": "fail",
"msg": "失败原因"
}
```
2. **动态码校验通过,等待移动端授权登录**:
```json
{
"state": "ok",
"step": "0"
}
```
3. **等待移动端授权超时**:
```json
{
"state": "ok",
"step": "1"
}
```
4. **移动端已授权登录**:
```json
{
"state": "ok",
"step": "2",
"openid": "xxx",
"head_img": "/xxx",
"sex": "男/女",
"company": "xxx",
"userid": 123,
"idnum": "xxx",
"username": "xxx",
"tel": "xxx"
}
```
### 2.6 退出登录
* **API**: `https://www.ab98.cn/api/doInterface?code=doLogout`
* **请求方式**: POST
* **Content-Type**: application/json
* **请求BODY**:
```json
{
"token":"358900e1005c33a1dd059b07042ceec3"
}
```
**成功返回**:
```json
{
"state": "ok"
}
```