shinnosuke
2026-08-06 79ae06e6b4172d90df77b0e49ad3ab12ba63cd45
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package com.talentcard.server.business.auth;
 
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class TalentAuthService {
 
    private static final SecureRandom RANDOM = new SecureRandom();
    private static final int ACCESS_TOKEN_HOURS = 2;
    private static final int REFRESH_TOKEN_DAYS = 30;
 
    private final JdbcTemplate jdbc;
    private final WeChatMiniProgramClient weChat;
 
    public TalentAuthService(JdbcTemplate jdbc, WeChatMiniProgramClient weChat) {
        this.jdbc = jdbc;
        this.weChat = weChat;
    }
 
    @Transactional
    public Map<String, Object> login(TalentLoginRequest request) {
        String openId = weChat.exchangeOpenId(request.wechatCode());
        String mobile = weChat.exchangePhoneNumber(request.phoneCode());
        return establishSession(openId, mobile);
    }
 
    @Transactional
    public Map<String, Object> createDevelopmentSession(long talentId) {
        List<Map<String, Object>> talents = rows("""
                select id, openid, status
                from talent_user
                where id = ? and deleted = 0
                limit 1 for update
                """, talentId);
        if (talents.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "开发测试人才账号不存在");
        if (number(talents.get(0).get("status")) != 1) {
            throw new BusinessException(ErrorCode.FORBIDDEN, "持卡人被停用");
        }
        Object openId = talents.get(0).get("openid");
        if (openId == null || String.valueOf(openId).isBlank()) {
            throw new BusinessException(ErrorCode.CONFLICT, "开发测试人才账号未配置微信标识");
        }
        return issueSession(talentId);
    }
 
    @Transactional
    public Map<String, Object> refresh(TalentRefreshRequest request) {
        List<Map<String, Object>> sessions = rows("""
                select s.id, s.talent_id
                from talent_session s
                join talent_user t on t.id = s.talent_id and t.deleted = 0 and t.status = 1
                where s.refresh_token_hash = ? and s.revoked_at is null and s.refresh_expires_at > now()
                limit 1 for update
                """, sha256(request.refreshToken().trim()));
        if (sessions.isEmpty()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录已失效,请重新登录");
        Map<String, Object> session = sessions.get(0);
        String accessToken = token();
        String refreshToken = token();
        LocalDateTime accessExpiresAt = LocalDateTime.now().plusHours(ACCESS_TOKEN_HOURS);
        LocalDateTime refreshExpiresAt = LocalDateTime.now().plusDays(REFRESH_TOKEN_DAYS);
        jdbc.update("""
                update talent_session
                set access_token_hash = ?, refresh_token_hash = ?, access_expires_at = ?, refresh_expires_at = ?, last_used_at = now()
                where id = ?
                """, sha256(accessToken), sha256(refreshToken), accessExpiresAt, refreshExpiresAt, session.get("id"));
        return sessionResult(number(session.get("talentId")), accessToken, refreshToken, accessExpiresAt);
    }
 
    @Transactional
    public Map<String, Object> logout(String authorization) {
        String token = bearerToken(authorization);
        if (!token.isBlank()) {
            jdbc.update("update talent_session set revoked_at = coalesce(revoked_at, now()) where access_token_hash = ?", sha256(token));
        }
        return Map.of("loggedOut", true);
    }
 
    @Transactional(readOnly = true)
    public TalentSessionPrincipal authenticate(String authorization) {
        String token = bearerToken(authorization);
        if (token.isBlank()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "请先登录");
        List<Map<String, Object>> sessions = rows("""
                select s.talent_id
                from talent_session s
                join talent_user t on t.id = s.talent_id and t.deleted = 0 and t.status = 1
                where s.access_token_hash = ? and s.revoked_at is null and s.access_expires_at > now()
                limit 1
                """, sha256(token));
        if (sessions.isEmpty()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录已失效,请重新登录");
        return new TalentSessionPrincipal(number(sessions.get(0).get("talentId")));
    }
 
    private Map<String, Object> establishSession(String openId, String mobile) {
        List<Map<String, Object>> talents = rows("""
                select id, openid, status
                from talent_user
                where mobile = ? and deleted = 0
                limit 1 for update
                """, mobile);
        if (talents.isEmpty()) return issueSession(registerVisitor(openId, mobile));
        Map<String, Object> talent = talents.get(0);
        if (number(talent.get("status")) != 1) {
            throw new BusinessException(ErrorCode.FORBIDDEN, "持卡人被停用");
        }
        long talentId = number(talent.get("id"));
        List<Map<String, Object>> binding = rows("""
                select t.id,
                       exists(select 1 from talent_card c where c.talent_id = t.id and c.deleted = 0 and c.card_type = 'PERSONAL') has_talent_card
                from talent_user t
                where t.openid = ? and t.deleted = 0
                limit 1 for update
                """, openId);
        if (!binding.isEmpty() && number(binding.get(0).get("id")) != talentId) {
            if (number(binding.get(0).get("hasTalentCard")) != 0) {
                throw new BusinessException(ErrorCode.CONFLICT, "当前微信已绑定其他人才账号,请联系管理员");
            }
            jdbc.update("update talent_user set openid = null where id = ?", number(binding.get(0).get("id")));
        }
        jdbc.update("update talent_user set openid = ? where id = ?", openId, talentId);
        return issueSession(talentId);
    }
 
    private long registerVisitor(String openId, String mobile) {
        List<Map<String, Object>> bindings = rows("""
                select id
                from talent_user
                where openid = ? and deleted = 0 and status = 1
                limit 1 for update
                """, openId);
        if (!bindings.isEmpty()) {
            long talentId = number(bindings.get(0).get("id"));
            jdbc.update("update talent_user set mobile = ? where id = ?", mobile, talentId);
            return talentId;
        }
        jdbc.update("""
                insert into talent_user(openid, name, mobile, talent_level, source_area_type, status)
                values (?, '普通游客', ?, '普通游客', 'LOCAL', 1)
                """, openId, mobile);
        Long talentId = jdbc.queryForObject("select last_insert_id()", Long.class);
        if (talentId == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "普通游客注册失败");
        return talentId;
    }
 
    private Map<String, Object> issueSession(long talentId) {
        jdbc.update("update talent_session set revoked_at = now() where talent_id = ? and revoked_at is null", talentId);
 
        String accessToken = token();
        String refreshToken = token();
        LocalDateTime accessExpiresAt = LocalDateTime.now().plusHours(ACCESS_TOKEN_HOURS);
        LocalDateTime refreshExpiresAt = LocalDateTime.now().plusDays(REFRESH_TOKEN_DAYS);
        jdbc.update("""
                insert into talent_session(talent_id, access_token_hash, refresh_token_hash, access_expires_at, refresh_expires_at, last_used_at)
                values (?, ?, ?, ?, ?, now())
                """, talentId, sha256(accessToken), sha256(refreshToken), accessExpiresAt, refreshExpiresAt);
        return sessionResult(talentId, accessToken, refreshToken, accessExpiresAt);
    }
 
    private Map<String, Object> sessionResult(long talentId, String accessToken, String refreshToken, LocalDateTime accessExpiresAt) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("talentId", talentId);
        result.put("accessToken", accessToken);
        result.put("refreshToken", refreshToken);
        result.put("expireAt", accessExpiresAt.atZone(java.time.ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli());
        return result;
    }
 
    private String token() {
        byte[] bytes = new byte[32];
        RANDOM.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
 
    private String bearerToken(String authorization) {
        return authorization == null ? "" : authorization.replaceFirst("(?i)^Bearer\\s+", "").trim();
    }
 
    private String sha256(String value) {
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
            StringBuilder result = new StringBuilder();
            for (byte item : digest) result.append(String.format("%02x", item));
            return result.toString();
        } catch (Exception exception) {
            throw new IllegalStateException("Unable to process session token", exception);
        }
    }
 
    private List<Map<String, Object>> rows(String sql, Object... args) {
        return DatabaseRows.normalize(jdbc.queryForList(sql, args));
    }
 
    private long number(Object value) {
        if (value instanceof Number number) return number.longValue();
        try { return Long.parseLong(String.valueOf(value)); } catch (Exception exception) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "人才会话数据异常"); }
    }
}