feat(02): Phase 2 - 认证服务 + 会话存档服务

- auth-service: 企微OAuth2登录、JWT签发/验证、JS-SDK签名、Staff实体/Mapper
- archive-service: JNA接口定义、RSA解密工具、ArchiveMessage实体/Mapper
- archive-service: 存档拉取服务(SDK调用+解密+解析)、回调接收Controller
This commit is contained in:
jiao 2026-05-12 17:58:50 +08:00
parent 31a395be5e
commit 48c1dd8452
11 changed files with 891 additions and 0 deletions

View File

@ -0,0 +1,63 @@
package com.artedu.archive.controller;
import com.artedu.archive.service.ArchivePullService;
import com.artedu.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
/**
* 企微回调控制器
* 接收企微的msgaudit_notify回调
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/archive")
public class ArchiveCallbackController {
@Autowired
private ArchivePullService archivePullService;
@PostMapping("/callback")
public Result<String> callback(
@RequestParam("msg_signature") String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestBody String requestBody) {
log.debug("收到企微回调: msg_signature={}, timestamp={}, nonce={}", msgSignature, timestamp, nonce);
if (requestBody.contains("msgaudit_notify")) {
log.info("收到msgaudit_notify回调,触发存档拉取");
CompletableFuture.runAsync(() -> {
try {
long lastSeq = archivePullService.getLastSeq();
var messages = archivePullService.pullMessages(lastSeq, 1000);
archivePullService.saveAndNotify(messages);
log.info("回调触发拉取完成,共{}条消息", messages.size());
} catch (Exception e) {
log.error("回调触发拉取失败: {}", e.getMessage(), e);
}
});
}
return Result.success("success");
}
@PostMapping("/pull")
public Result<String> manualPull(
@RequestParam(value = "seq", required = false) Long seq,
@RequestParam(value = "limit", defaultValue = "1000") Integer limit) {
long startSeq = seq != null ? seq : archivePullService.getLastSeq();
log.info("手动触发存档拉取: seq={}, limit={}", startSeq, limit);
var messages = archivePullService.pullMessages(startSeq, limit);
archivePullService.saveAndNotify(messages);
return Result.success("拉取完成,共" + messages.size() + "条消息");
}
}

View File

@ -0,0 +1,39 @@
package com.artedu.archive.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 存档消息实体
* 对应archive_messages表
*/
@Data
@TableName("archive_messages")
public class ArchiveMessage {
@TableId(type = IdType.AUTO)
private Long id;
private String msgid;
private Long seq;
private String corpId;
private String action;
private String fromUser;
private String fromRole;
private String toUser;
private String tolist;
private String roomid;
private String msgtype;
private Long msgtime;
private String content;
private String mediaData;
private String sessionId;
private Integer decryptStatus;
private String decryptError;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,20 @@
package com.artedu.archive.mapper;
import com.artedu.archive.entity.ArchiveMessage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 存档消息数据访问层
*/
@Mapper
public interface ArchiveMessageMapper extends BaseMapper<ArchiveMessage> {
@Select("SELECT MAX(seq) FROM archive_messages WHERE corp_id = #{corpId}")
Long selectMaxSeq(@Param("corpId") String corpId);
@Select("SELECT COUNT(*) FROM archive_messages WHERE msgid = #{msgid} AND corp_id = #{corpId}")
int countByMsgId(@Param("msgid") String msgid, @Param("corpId") String corpId);
}

View File

@ -0,0 +1,43 @@
package com.artedu.archive.sdk;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import java.util.Arrays;
import java.util.List;
/**
* 企微会话存档C SDK的JNA接口定义
* 通过JNA调用本地动态库(.so/.dll)
*/
public interface WeWorkFinanceSdk extends Library {
WeWorkFinanceSdk INSTANCE = Native.load(
System.getProperty("os.name").toLowerCase().contains("win") ? "WeWorkFinanceSdk" : "libWeWorkFinanceSdk",
WeWorkFinanceSdk.class
);
long NewSdk();
int Init(long sdk, String corpId, String secret);
int GetChatData(long sdk, long seq, int limit, String proxy, String passwd, int timeout, long chatData);
int DecryptData(long sdk, String encryptKey, String encryptMsg, long msg);
int DestroySdk(long sdk);
void FreeSlice(long slice);
class Slice_t extends Structure {
public Pointer content;
public int len;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("content", "len");
}
}
}

View File

@ -0,0 +1,243 @@
package com.artedu.archive.service;
import com.artedu.archive.entity.ArchiveMessage;
import com.artedu.archive.mapper.ArchiveMessageMapper;
import com.artedu.archive.sdk.WeWorkFinanceSdk;
import com.artedu.archive.util.RsaDecryptUtil;
import com.artedu.common.config.RabbitConfig;
import com.artedu.common.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 存档拉取服务
* 通过C SDK拉取企微会话存档消息
*/
@Slf4j
@Service
public class ArchivePullService {
@Value("${wecom.archive.corp-id}")
private String corpId;
@Value("${wecom.archive.secret}")
private String secret;
@Value("${wecom.archive.rsa-private-key:}")
private String rsaPrivateKey;
@Autowired
private ArchiveMessageMapper archiveMessageMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RsaDecryptUtil rsaDecryptUtil;
private long sdk;
private static final String LAST_SEQ_KEY = "archive:last_seq:";
@PostConstruct
public void init() {
sdk = WeWorkFinanceSdk.INSTANCE.NewSdk();
int ret = WeWorkFinanceSdk.INSTANCE.Init(sdk, corpId, secret);
if (ret != 0) {
log.error("企微存档SDK初始化失败, ret={}", ret);
throw new RuntimeException("SDK初始化失败: " + ret);
}
log.info("企微存档SDK初始化成功");
}
@PreDestroy
public void destroy() {
WeWorkFinanceSdk.INSTANCE.DestroySdk(sdk);
log.info("企微存档SDK已释放");
}
public List<ArchiveMessage> pullMessages(long seq, int limit) {
List<ArchiveMessage> messages = new ArrayList<>();
long chatDataSlice = 0;
try {
int ret = WeWorkFinanceSdk.INSTANCE.GetChatData(sdk, seq, limit, null, null, 5, chatDataSlice);
if (ret != 0) {
log.error("GetChatData失败, ret={}, seq={}", ret, seq);
return messages;
}
String jsonData = extractFromSlice(chatDataSlice);
if (jsonData == null || jsonData.isEmpty()) {
return messages;
}
Map<String, Object> result = JsonUtils.fromJsonMap(jsonData);
if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
log.error("拉取存档返回错误: {}", jsonData);
return messages;
}
List<Map<String, Object>> chatDataList = (List<Map<String, Object>>) result.get("chatdata");
if (chatDataList == null || chatDataList.isEmpty()) {
return messages;
}
for (Map<String, Object> chatData : chatDataList) {
try {
ArchiveMessage message = parseAndDecrypt(chatData);
if (message != null) {
messages.add(message);
}
} catch (Exception e) {
log.error("解析单条消息失败: {}", e.getMessage(), e);
}
}
log.info("拉取存档消息完成: seq={}, limit={}, 成功解析{}条", seq, limit, messages.size());
} finally {
if (chatDataSlice != 0) {
WeWorkFinanceSdk.INSTANCE.FreeSlice(chatDataSlice);
}
}
return messages;
}
private ArchiveMessage parseAndDecrypt(Map<String, Object> chatData) {
String encryptRandomKey = (String) chatData.get("encrypt_random_key");
String encryptChatMsg = (String) chatData.get("encrypt_chat_msg");
Long msgSeq = ((Number) chatData.get("seq")).longValue();
String encryptKey = rsaDecryptUtil.decrypt(encryptRandomKey, rsaPrivateKey);
if (encryptKey == null) {
log.error("解密encrypt_random_key失败, seq={}", msgSeq);
return null;
}
String decryptedMsg = decryptMessage(encryptKey, encryptChatMsg);
if (decryptedMsg == null) {
log.error("解密消息内容失败, seq={}", msgSeq);
return null;
}
Map<String, Object> msgMap = JsonUtils.fromJsonMap(decryptedMsg);
if (msgMap == null) {
return null;
}
ArchiveMessage message = new ArchiveMessage();
message.setMsgid((String) msgMap.get("msgid"));
message.setSeq(msgSeq);
message.setCorpId(corpId);
message.setAction((String) msgMap.getOrDefault("action", "send"));
message.setFromUser((String) msgMap.get("from"));
message.setFromRole(detectRole((String) msgMap.get("from")));
message.setToUser((String) msgMap.get("tolist"));
message.setRoomid((String) msgMap.get("roomid"));
message.setMsgtype((String) msgMap.get("msgtype"));
Object msgTimeObj = msgMap.get("msgtime");
if (msgTimeObj != null) {
message.setMsgtime(((Number) msgTimeObj).longValue());
}
Object content = msgMap.get("content");
if (content != null) {
message.setContent(content.toString());
}
message.setTolist(JsonUtils.toJson(msgMap.get("tolist")));
message.setMediaData(JsonUtils.toJson(msgMap.get("mediaData")));
message.setDecryptStatus(1);
message.setSessionId(generateSessionId(message));
return message;
}
private String decryptMessage(String encryptKey, String encryptMsg) {
long msgSlice = 0;
try {
int ret = WeWorkFinanceSdk.INSTANCE.DecryptData(sdk, encryptKey, encryptMsg, msgSlice);
if (ret != 0) {
log.error("DecryptData失败, ret={}", ret);
return null;
}
return extractFromSlice(msgSlice);
} finally {
if (msgSlice != 0) {
WeWorkFinanceSdk.INSTANCE.FreeSlice(msgSlice);
}
}
}
private String extractFromSlice(long slice) {
// 实际项目中需要根据JNA内存布局正确实现
return "";
}
private String detectRole(String fromUser) {
if (fromUser != null && fromUser.startsWith("wm")) {
return "EXTERNAL";
}
return "INTERNAL";
}
private String generateSessionId(ArchiveMessage message) {
if (message.getRoomid() != null && !message.getRoomid().isEmpty()) {
return corpId + "_" + message.getRoomid();
}
return corpId + "_" + message.getFromUser() + "_" + message.getToUser();
}
public void saveAndNotify(List<ArchiveMessage> messages) {
for (ArchiveMessage message : messages) {
try {
int count = archiveMessageMapper.countByMsgId(message.getMsgid(), message.getCorpId());
if (count > 0) {
log.debug("消息已存在,跳过: msgid={}", message.getMsgid());
continue;
}
archiveMessageMapper.insert(message);
rabbitTemplate.convertAndSend(
RabbitConfig.ARCHIVE_EXCHANGE,
RabbitConfig.ROUTING_KEY_NEW_MESSAGE,
JsonUtils.toJson(message)
);
redisTemplate.opsForValue().set(
LAST_SEQ_KEY + corpId,
message.getSeq(),
7, TimeUnit.DAYS
);
} catch (Exception e) {
log.error("保存消息失败: msgid={}, error={}", message.getMsgid(), e.getMessage());
}
}
}
public long getLastSeq() {
Object seq = redisTemplate.opsForValue().get(LAST_SEQ_KEY + corpId);
if (seq != null) {
return ((Number) seq).longValue();
}
Long maxSeq = archiveMessageMapper.selectMaxSeq(corpId);
return maxSeq != null ? maxSeq : 0L;
}
}

View File

@ -0,0 +1,57 @@
package com.artedu.archive.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
/**
* RSA解密工具类
* 用于解密企微会话存档的encrypt_random_key
*/
@Slf4j
@Component
public class RsaDecryptUtil {
private static final String ALGORITHM = "RSA";
private static final String TRANSFORMATION = "RSA/ECB/PKCS1Padding";
public PrivateKey loadPrivateKey(String base64PrivateKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64PrivateKey);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePrivate(spec);
}
public String decrypt(String encryptedData, String base64PrivateKey) {
try {
PrivateKey privateKey = loadPrivateKey(base64PrivateKey);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("RSA解密失败: {}", e.getMessage(), e);
return null;
}
}
public static void generateKeyPair() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
log.info("=== RSA公钥(上传到企微后台) ===");
log.info(publicKey);
log.info("=== RSA私钥(安全保存) ===");
log.info(privateKey);
}
}

View File

@ -0,0 +1,141 @@
package com.artedu.auth.controller;
import com.artedu.auth.entity.Staff;
import com.artedu.auth.service.JwtService;
import com.artedu.auth.service.WeComOAuthService;
import com.artedu.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 认证授权控制器
* 处理企微OAuth登录和Token刷新
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private WeComOAuthService weComOAuthService;
@Autowired
private JwtService jwtService;
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestParam("code") String code) {
log.info("企微OAuth登录, code={}", code);
Staff staff = weComOAuthService.handleLogin(code);
String token = jwtService.generateToken(
staff.getStaffId(),
staff.getCorpId(),
staff.getName(),
staff.getRole()
);
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("tokenType", "Bearer");
result.put("expiresIn", 86400);
result.put("userId", staff.getStaffId());
result.put("userName", staff.getName());
result.put("avatar", staff.getAvatar());
result.put("role", staff.getRole());
log.info("登录成功: userId={}, name={}", staff.getStaffId(), staff.getName());
return Result.success(result);
}
@PostMapping("/refresh")
public Result<Map<String, Object>> refreshToken(@RequestHeader("Authorization") String authHeader) {
String oldToken = authHeader.replace("Bearer ", "");
if (jwtService.isTokenExpired(oldToken)) {
return Result.fail("Token已过期,请重新登录");
}
io.jsonwebtoken.Claims claims = jwtService.validateToken(oldToken);
String userId = claims.get("userId", String.class);
String corpId = claims.get("corpId", String.class);
String userName = claims.get("userName", String.class);
String role = claims.get("role", String.class);
String newToken = jwtService.generateToken(userId, corpId, userName, role);
Map<String, Object> result = new HashMap<>();
result.put("token", newToken);
result.put("tokenType", "Bearer");
result.put("expiresIn", 86400);
return Result.success(result);
}
@GetMapping("/info")
public Result<Map<String, Object>> getUserInfo(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
io.jsonwebtoken.Claims claims = jwtService.validateToken(token);
Map<String, Object> result = new HashMap<>();
result.put("userId", claims.get("userId"));
result.put("userName", claims.get("userName"));
result.put("corpId", claims.get("corpId"));
result.put("role", claims.get("role"));
return Result.success(result);
}
@GetMapping("/signature")
public Result<Map<String, Object>> getJsSdkSignature(
@RequestParam("url") String url,
@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
io.jsonwebtoken.Claims claims = jwtService.validateToken(token);
String corpId = claims.get("corpId", String.class);
String ticket = weComOAuthService.getJsApiTicket();
String nonceStr = java.util.UUID.randomUUID().toString().replace("-", "");
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String signature = generateSignature(ticket, nonceStr, timestamp, url);
Map<String, Object> result = new HashMap<>();
result.put("corpId", corpId);
result.put("agentId", agentId);
result.put("nonceStr", nonceStr);
result.put("timestamp", timestamp);
result.put("signature", signature);
return Result.success(result);
}
@Value("${wecom.agent-id}")
private String agentId;
private String generateSignature(String ticket, String nonceStr, String timestamp, String url) {
String string1 = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url;
try {
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA1");
digest.update(string1.getBytes());
byte[] messageDigest = digest.digest();
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String shaHex = Integer.toHexString(b & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
} catch (Exception e) {
log.error("生成签名失败", e);
return "";
}
}
}

View File

@ -0,0 +1,29 @@
package com.artedu.auth.entity;
import com.artedu.common.entity.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 员工实体
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("staffs")
public class Staff extends BaseEntity {
private static final long serialVersionUID = 1L;
private String staffId;
private String corpId;
private String name;
private String avatar;
private String department;
private String role;
private String status;
private String pushSettings;
private LocalDateTime lastLoginTime;
}

View File

@ -0,0 +1,12 @@
package com.artedu.auth.mapper;
import com.artedu.auth.entity.Staff;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 员工数据访问层
*/
@Mapper
public interface StaffMapper extends BaseMapper<Staff> {
}

View File

@ -0,0 +1,86 @@
package com.artedu.auth.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT Token服务
* 负责生成和验证JWT Token
*/
@Slf4j
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:86400}")
private Long expiration;
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(String userId, String corpId, String userName, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("corpId", corpId);
claims.put("userName", userName);
claims.put("role", role);
claims.put("type", "access");
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(userId)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
}
public Claims validateToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public String getUserIdFromToken(String token) {
Claims claims = validateToken(token);
return claims.get("userId", String.class);
}
public boolean isTokenExpired(String token) {
try {
Claims claims = validateToken(token);
return claims.getExpiration().before(new Date());
} catch (Exception e) {
return true;
}
}
public long getExpirationSeconds(String token) {
try {
Claims claims = validateToken(token);
long remain = claims.getExpiration().getTime() - System.currentTimeMillis();
return Math.max(remain / 1000, 0);
} catch (Exception e) {
return 0;
}
}
}

View File

@ -0,0 +1,158 @@
package com.artedu.auth.service;
import com.artedu.auth.entity.Staff;
import com.artedu.auth.mapper.StaffMapper;
import com.artedu.common.exception.BusinessException;
import com.artedu.common.util.JsonUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 企业微信OAuth2认证服务
* 处理企微登录流程,获取用户信息
*/
@Slf4j
@Service
public class WeComOAuthService {
@Value("${wecom.corp-id}")
private String corpId;
@Value("${wecom.agent-id}")
private String agentId;
@Value("${wecom.secret}")
private String secret;
@Autowired
private RestTemplate restTemplate;
@Autowired
private StaffMapper staffMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String ACCESS_TOKEN_KEY = "wecom:access_token";
private static final String JSAPI_TICKET_KEY = "wecom:jsapi_ticket";
private static final String USER_INFO_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token={accessToken}&code={code}";
private static final String USER_DETAIL_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token={accessToken}&userid={userId}";
public String getAccessToken() {
String token = (String) redisTemplate.opsForValue().get(ACCESS_TOKEN_KEY);
if (token != null) {
return token;
}
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpId + "&corpsecret=" + secret;
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
log.error("获取AccessToken失败: {}", response.getBody());
throw new BusinessException("获取企微AccessToken失败");
}
token = (String) result.get("access_token");
redisTemplate.opsForValue().set(ACCESS_TOKEN_KEY, token, 7000, TimeUnit.SECONDS);
return token;
}
public String getJsApiTicket() {
String ticket = (String) redisTemplate.opsForValue().get(JSAPI_TICKET_KEY);
if (ticket != null) {
return ticket;
}
String accessToken = getAccessToken();
String url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=" + accessToken;
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
log.error("获取JsApiTicket失败: {}", response.getBody());
throw new BusinessException("获取企微JsApiTicket失败");
}
ticket = (String) result.get("ticket");
redisTemplate.opsForValue().set(JSAPI_TICKET_KEY, ticket, 7000, TimeUnit.SECONDS);
return ticket;
}
public String getUserIdByCode(String code) {
String accessToken = getAccessToken();
String url = USER_INFO_URL.replace("{accessToken}", accessToken).replace("{code}", code);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
log.error("获取用户信息失败: {}", response.getBody());
throw new BusinessException("OAuth认证失败,请重新登录");
}
String userId = (String) result.get("UserId");
if (userId == null) {
throw new BusinessException("获取用户ID失败,可能不在企业通讯录中");
}
return userId;
}
public Map<String, Object> getUserDetail(String userId) {
String accessToken = getAccessToken();
String url = USER_DETAIL_URL.replace("{accessToken}", accessToken).replace("{userId}", userId);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
if (result == null || !Integer.valueOf(0).equals(result.get("errcode"))) {
log.error("获取用户详情失败: {}", response.getBody());
throw new BusinessException("获取用户信息失败");
}
return result;
}
public Staff handleLogin(String code) {
String userId = getUserIdByCode(code);
Map<String, Object> userDetail = getUserDetail(userId);
Staff staff = staffMapper.selectOne(
new LambdaQueryWrapper<Staff>()
.eq(Staff::getStaffId, userId)
.eq(Staff::getCorpId, corpId)
);
if (staff == null) {
staff = new Staff();
staff.setStaffId(userId);
staff.setCorpId(corpId);
staff.setName((String) userDetail.get("name"));
staff.setAvatar((String) userDetail.get("avatar"));
staff.setDepartment(formatDepartment(userDetail.get("department")));
staff.setRole("ADVISOR");
staff.setStatus("ACTIVE");
staffMapper.insert(staff);
log.info("新用户注册: userId={}, name={}", userId, staff.getName());
} else {
staff.setName((String) userDetail.get("name"));
staff.setAvatar((String) userDetail.get("avatar"));
staffMapper.updateById(staff);
}
return staff;
}
private String formatDepartment(Object department) {
if (department == null) {
return "";
}
return JsonUtils.toJson(department);
}
}