feat: add h5 account auth flow
This commit is contained in:
1 parent
ef6346a563
commit
1f612485a0
13 files changed
+774
-51
No files matched your search
@@ -2,20 +2,29 @@ package xuan.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import xuan.entity.UserInfo;
|
||||
import xuan.service.UserInfoService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@@ -30,6 +39,15 @@ public class UserController {
|
||||
|
||||
@Value("${wechat.secret:}")
|
||||
private String appSecret;
|
||||
|
||||
@Value("${wechat.web.appid:}")
|
||||
private String webAppId;
|
||||
|
||||
@Value("${wechat.web.secret:}")
|
||||
private String webAppSecret;
|
||||
|
||||
@Value("${wechat.web.callback-base-url:}")
|
||||
private String webCallbackBaseUrl;
|
||||
|
||||
@PostMapping("/login")
|
||||
public Map<String, Object> login(@RequestBody Map<String, Object> params) {
|
||||
@@ -90,23 +108,10 @@ public class UserController {
|
||||
}
|
||||
|
||||
UserInfo user = userInfoService.loginOrRegister(openId, username, avatarUrl);
|
||||
|
||||
System.out.println("登录成功: userId=" + user.getUserId() + ", openId=" + user.getOpenId());
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("id", user.getId());
|
||||
data.put("userId", user.getUserId());
|
||||
data.put("openId", user.getOpenId());
|
||||
data.put("avatarUrl", user.getAvatarUrl());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("phone", user.getPhone());
|
||||
data.put("vipLevel", user.getVipLevel());
|
||||
data.put("coins", user.getCoins());
|
||||
data.put("streakDays", user.getStreakDays());
|
||||
data.put("createTime", user.getCreateTime());
|
||||
|
||||
|
||||
result.put("success", true);
|
||||
result.put("data", data);
|
||||
result.put("data", buildUserData(user));
|
||||
} catch (Exception e) {
|
||||
System.err.println("登录异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
@@ -115,6 +120,117 @@ public class UserController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("/wechat/h5/start")
|
||||
public ResponseEntity<Void> startWechatH5Login(
|
||||
@RequestParam(required = false) String returnUrl,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
String targetReturnUrl = StringUtils.hasText(returnUrl)
|
||||
? returnUrl
|
||||
: resolveDefaultFrontendReturnUrl(request);
|
||||
|
||||
if (!StringUtils.hasText(webAppId) || !StringUtils.hasText(webAppSecret)) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未配置微信网站应用登录"));
|
||||
}
|
||||
|
||||
String callbackBaseUrl = StringUtils.hasText(webCallbackBaseUrl)
|
||||
? webCallbackBaseUrl
|
||||
: resolveRequestBaseUrl(request);
|
||||
|
||||
String callbackUrl = UriComponentsBuilder
|
||||
.fromHttpUrl(callbackBaseUrl + "/api/user/wechat/h5/callback")
|
||||
.queryParam("returnUrl", targetReturnUrl)
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
String wechatLoginUrl = UriComponentsBuilder
|
||||
.fromHttpUrl("https://open.weixin.qq.com/connect/qrconnect")
|
||||
.queryParam("appid", webAppId)
|
||||
.queryParam("redirect_uri", callbackUrl)
|
||||
.queryParam("response_type", "code")
|
||||
.queryParam("scope", "snsapi_login")
|
||||
.queryParam("state", "xuan_h5_login")
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString() + "#wechat_redirect";
|
||||
|
||||
return redirect(wechatLoginUrl);
|
||||
}
|
||||
|
||||
@GetMapping("/wechat/h5/callback")
|
||||
public ResponseEntity<Void> handleWechatH5Callback(
|
||||
@RequestParam(required = false) String code,
|
||||
@RequestParam(required = false) String returnUrl
|
||||
) {
|
||||
String targetReturnUrl = StringUtils.hasText(returnUrl)
|
||||
? returnUrl
|
||||
: "/";
|
||||
|
||||
if (!StringUtils.hasText(webAppId) || !StringUtils.hasText(webAppSecret)) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未配置微信网站应用登录"));
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(code)) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "未获取到微信登录凭证"));
|
||||
}
|
||||
|
||||
try {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
String accessTokenUrl = UriComponentsBuilder
|
||||
.fromHttpUrl("https://api.weixin.qq.com/sns/oauth2/access_token")
|
||||
.queryParam("appid", webAppId)
|
||||
.queryParam("secret", webAppSecret)
|
||||
.queryParam("code", code)
|
||||
.queryParam("grant_type", "authorization_code")
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> tokenResult = restTemplate.getForObject(accessTokenUrl, Map.class);
|
||||
if (tokenResult == null || !tokenResult.containsKey("openid") || !tokenResult.containsKey("access_token")) {
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, extractWechatError(tokenResult, "微信扫码登录失败")));
|
||||
}
|
||||
|
||||
String openId = String.valueOf(tokenResult.get("openid"));
|
||||
String accessToken = String.valueOf(tokenResult.get("access_token"));
|
||||
|
||||
String username = "微信用户";
|
||||
String avatarUrl = "";
|
||||
String userInfoUrl = UriComponentsBuilder
|
||||
.fromHttpUrl("https://api.weixin.qq.com/sns/userinfo")
|
||||
.queryParam("access_token", accessToken)
|
||||
.queryParam("openid", openId)
|
||||
.queryParam("lang", "zh_CN")
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> userInfoResult = restTemplate.getForObject(userInfoUrl, Map.class);
|
||||
if (userInfoResult != null && !userInfoResult.containsKey("errcode")) {
|
||||
if (userInfoResult.get("nickname") != null) {
|
||||
username = String.valueOf(userInfoResult.get("nickname"));
|
||||
}
|
||||
if (userInfoResult.get("headimgurl") != null) {
|
||||
avatarUrl = String.valueOf(userInfoResult.get("headimgurl"));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取微信用户信息失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
UserInfo user = userInfoService.loginOrRegister(openId, username, avatarUrl);
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, true, null, buildUserData(user)));
|
||||
} catch (Exception e) {
|
||||
System.err.println("H5微信扫码回调异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return redirect(buildFrontendRedirectUrl(targetReturnUrl, false, "微信扫码登录失败,请重试"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/info")
|
||||
public Map<String, Object> getUserInfo(@RequestParam String userId) {
|
||||
@@ -146,6 +262,41 @@ public class UserController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/h5/register")
|
||||
public Map<String, Object> registerH5(@RequestBody Map<String, Object> params) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
String loginName = params.get("loginName") != null ? params.get("loginName").toString() : "";
|
||||
String nickname = params.get("nickname") != null ? params.get("nickname").toString() : "";
|
||||
String password = params.get("password") != null ? params.get("password").toString() : "";
|
||||
|
||||
UserInfo user = userInfoService.registerWithPassword(loginName, nickname, password);
|
||||
result.put("success", true);
|
||||
result.put("data", buildUserData(user));
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/h5/login")
|
||||
public Map<String, Object> loginH5(@RequestBody Map<String, Object> params) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
String loginName = params.get("loginName") != null ? params.get("loginName").toString() : "";
|
||||
String password = params.get("password") != null ? params.get("password").toString() : "";
|
||||
|
||||
UserInfo user = userInfoService.loginWithPassword(loginName, password);
|
||||
result.put("success", true);
|
||||
result.put("data", buildUserData(user));
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/updateVip")
|
||||
public Map<String, Object> updateVipLevel(@RequestBody Map<String, Object> params) {
|
||||
@@ -407,4 +558,109 @@ public class UserController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildUserData(UserInfo user) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("id", user.getId());
|
||||
data.put("userId", user.getUserId());
|
||||
data.put("openId", user.getOpenId());
|
||||
data.put("avatarUrl", user.getAvatarUrl());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("loginName", user.getLoginName());
|
||||
data.put("phone", user.getPhone());
|
||||
data.put("vipLevel", user.getVipLevel());
|
||||
data.put("coins", user.getCoins());
|
||||
data.put("streakDays", user.getStreakDays());
|
||||
data.put("createTime", user.getCreateTime());
|
||||
return data;
|
||||
}
|
||||
|
||||
private ResponseEntity<Void> redirect(String targetUrl) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setLocation(URI.create(targetUrl));
|
||||
return new ResponseEntity<>(headers, HttpStatus.FOUND);
|
||||
}
|
||||
|
||||
private String buildFrontendRedirectUrl(String returnUrl, boolean success, String errorMessage) {
|
||||
return buildFrontendRedirectUrl(returnUrl, success, errorMessage, null);
|
||||
}
|
||||
|
||||
private String buildFrontendRedirectUrl(
|
||||
String returnUrl,
|
||||
boolean success,
|
||||
String errorMessage,
|
||||
Map<String, Object> payload
|
||||
) {
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
params.put("success", success ? "1" : "0");
|
||||
if (!success && StringUtils.hasText(errorMessage)) {
|
||||
params.put("error", errorMessage);
|
||||
}
|
||||
if (payload != null) {
|
||||
params.putAll(payload);
|
||||
}
|
||||
return appendParamsToUrl(returnUrl, params);
|
||||
}
|
||||
|
||||
private String appendParamsToUrl(String url, Map<String, Object> params) {
|
||||
if (!StringUtils.hasText(url)) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
int hashIndex = url.indexOf('#');
|
||||
if (hashIndex >= 0) {
|
||||
String prefix = url.substring(0, hashIndex);
|
||||
String fragment = url.substring(hashIndex + 1);
|
||||
String separator = fragment.contains("?") ? "&" : "?";
|
||||
return prefix + "#" + fragment + separator + toQueryString(params);
|
||||
}
|
||||
|
||||
String separator = url.contains("?") ? "&" : "?";
|
||||
return url + separator + toQueryString(params);
|
||||
}
|
||||
|
||||
private String toQueryString(Map<String, Object> params) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
if (entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append("&");
|
||||
}
|
||||
builder
|
||||
.append(urlEncode(entry.getKey()))
|
||||
.append("=")
|
||||
.append(urlEncode(String.valueOf(entry.getValue())));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String urlEncode(String value) {
|
||||
try {
|
||||
return URLEncoder.encode(value, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("URL编码失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractWechatError(Map<String, Object> wxResult, String fallback) {
|
||||
if (wxResult == null) {
|
||||
return fallback;
|
||||
}
|
||||
Object errMsg = wxResult.get("errmsg");
|
||||
if (errMsg != null && StringUtils.hasText(String.valueOf(errMsg))) {
|
||||
return String.valueOf(errMsg);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private String resolveRequestBaseUrl(HttpServletRequest request) {
|
||||
return request.getScheme() + "://" + request.getServerName()
|
||||
+ (request.getServerPort() == 80 || request.getServerPort() == 443 ? "" : ":" + request.getServerPort());
|
||||
}
|
||||
|
||||
private String resolveDefaultFrontendReturnUrl(HttpServletRequest request) {
|
||||
return resolveRequestBaseUrl(request) + "/#/pages/auth/wechat-callback";
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,12 @@ public class UserInfo {
|
||||
|
||||
@Column(length = 50)
|
||||
private String username;
|
||||
|
||||
@Column(unique = true, length = 50)
|
||||
private String loginName;
|
||||
|
||||
@Column(length = 64)
|
||||
private String passwordHash;
|
||||
|
||||
@Column(length = 20)
|
||||
private String userId;
|
||||
@@ -65,6 +71,12 @@ public class UserInfo {
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getLoginName() { return loginName; }
|
||||
public void setLoginName(String loginName) { this.loginName = loginName; }
|
||||
|
||||
public String getPasswordHash() { return passwordHash; }
|
||||
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
|
||||
@@ -8,4 +8,5 @@ import xuan.entity.UserInfo;
|
||||
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {
|
||||
UserInfo findByOpenId(String openId);
|
||||
UserInfo findByUserId(String userId);
|
||||
UserInfo findByLoginName(String loginName);
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import xuan.entity.CoinConsumeRecord;
|
||||
import xuan.repository.UserInfoRepository;
|
||||
import xuan.repository.CoinConsumeRecordRepository;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
@@ -40,6 +42,53 @@ public class UserInfoService {
|
||||
|
||||
return userInfoRepository.save(user);
|
||||
}
|
||||
|
||||
public UserInfo registerWithPassword(String loginName, String nickname, String password) {
|
||||
if (loginName == null || loginName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入用户名");
|
||||
}
|
||||
if (nickname == null || nickname.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入昵称");
|
||||
}
|
||||
if (password == null || password.length() < 6) {
|
||||
throw new IllegalArgumentException("密码至少6位");
|
||||
}
|
||||
|
||||
String normalizedLoginName = loginName.trim();
|
||||
if (userInfoRepository.findByLoginName(normalizedLoginName) != null) {
|
||||
throw new IllegalArgumentException("用户名已存在");
|
||||
}
|
||||
|
||||
UserInfo user = new UserInfo();
|
||||
user.setOpenId("h5_" + normalizedLoginName);
|
||||
user.setLoginName(normalizedLoginName);
|
||||
user.setPasswordHash(hashPassword(password));
|
||||
user.setUsername(nickname.trim());
|
||||
user.setAvatarUrl("");
|
||||
user.setUserId(generateUserId());
|
||||
user.setVipLevel(0);
|
||||
user.setStreakDays(0);
|
||||
|
||||
return userInfoRepository.save(user);
|
||||
}
|
||||
|
||||
public UserInfo loginWithPassword(String loginName, String password) {
|
||||
if (loginName == null || loginName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入用户名");
|
||||
}
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new IllegalArgumentException("请输入密码");
|
||||
}
|
||||
|
||||
UserInfo user = userInfoRepository.findByLoginName(loginName.trim());
|
||||
if (user == null || user.getPasswordHash() == null) {
|
||||
throw new IllegalArgumentException("用户名或密码错误");
|
||||
}
|
||||
if (!user.getPasswordHash().equals(hashPassword(password))) {
|
||||
throw new IllegalArgumentException("用户名或密码错误");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public UserInfo getUserInfo(String userId) {
|
||||
return userInfoRepository.findByUserId(userId);
|
||||
@@ -175,4 +224,18 @@ public class UserInfoService {
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String hashPassword(String password) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] bytes = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("密码处理失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ spring.datasource.password=xuan_password
|
||||
|
||||
# JPA Configuration
|
||||
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
|
||||
spring.jpa.hibernate.ddl-auto=create
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
|
||||
@@ -17,5 +17,10 @@ spring.jpa.properties.hibernate.format_sql=true
|
||||
logging.level.xuan=DEBUG
|
||||
|
||||
# WeChat Mini Program Configuration
|
||||
wechat.appid=wxbae101ca6e1f61e6
|
||||
wechat.secret=26f414d93627acf3e1d895231296b313
|
||||
wechat.appid=${WECHAT_APPID:}
|
||||
wechat.secret=${WECHAT_SECRET:}
|
||||
|
||||
# WeChat H5 QR Login Configuration
|
||||
wechat.web.appid=${WECHAT_WEB_APPID:${WECHAT_APPID:}}
|
||||
wechat.web.secret=${WECHAT_WEB_SECRET:${WECHAT_SECRET:}}
|
||||
wechat.web.callback-base-url=${WECHAT_WEB_CALLBACK_BASE_URL:https://api.bemover.xyz}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>排盘工具</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,13 +18,74 @@
|
||||
<image v-if="avatarUrl" :src="avatarUrl" class="header-avatar" mode="aspectFill"></image>
|
||||
<text v-else class="icon-emoji">🔐</text>
|
||||
</view>
|
||||
<text class="header-title">微信授权登录</text>
|
||||
<text class="header-title">{{ isH5 ? '账号登录' : '微信授权登录' }}</text>
|
||||
<text class="header-subtitle">{{ getStatusText() }}</text>
|
||||
</view>
|
||||
|
||||
<view class="panel-body">
|
||||
<view v-if="isH5" class="step-content">
|
||||
<view class="auth-mode-switch">
|
||||
<view
|
||||
class="mode-item"
|
||||
:class="{ active: h5Mode === 'register' }"
|
||||
@click="setH5Mode('register')"
|
||||
>
|
||||
注册
|
||||
</view>
|
||||
<view
|
||||
class="mode-item"
|
||||
:class="{ active: h5Mode === 'login' }"
|
||||
@click="setH5Mode('login')"
|
||||
>
|
||||
登录
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="h5-form-card">
|
||||
<input
|
||||
v-model.trim="h5LoginName"
|
||||
class="h5-input"
|
||||
maxlength="20"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
<input
|
||||
v-if="h5Mode === 'register'"
|
||||
v-model.trim="h5Nickname"
|
||||
class="h5-input"
|
||||
maxlength="20"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
<input
|
||||
v-model="h5Password"
|
||||
class="h5-input"
|
||||
password
|
||||
maxlength="32"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<input
|
||||
v-if="h5Mode === 'register'"
|
||||
v-model="h5ConfirmPassword"
|
||||
class="h5-input"
|
||||
password
|
||||
maxlength="32"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
<text class="h5-form-tip">
|
||||
{{ h5Mode === 'register' ? '注册后将自动登录,密码至少 6 位。' : '输入用户名和密码登录已有账号。' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<button class="auth-btn confirm-btn" @click="handleH5Auth" :disabled="loading">
|
||||
<text class="btn-text">{{ loading ? '提交中...' : (h5Mode === 'register' ? '注册并登录' : '登录') }}</text>
|
||||
</button>
|
||||
|
||||
<button class="auth-btn cancel-btn" @click="handleCancel">
|
||||
<text class="cancel-text">取消登录</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 未选择头像时:显示授权按钮 -->
|
||||
<view v-if="!avatarUrl" class="step-content">
|
||||
<view v-else-if="!avatarUrl" class="step-content">
|
||||
<view class="auth-tips">
|
||||
<view class="tip-item">
|
||||
<text class="tip-icon">1</text>
|
||||
@@ -98,6 +159,7 @@
|
||||
|
||||
<script>
|
||||
import api from '@/utils/api.js'
|
||||
import authCheck from '@/utils/authCheck.js'
|
||||
|
||||
export default {
|
||||
name: 'AuthModal',
|
||||
@@ -113,15 +175,27 @@ export default {
|
||||
avatarUrl: '',
|
||||
nickname: '',
|
||||
nicknameFocus: false,
|
||||
loading: false
|
||||
loading: false,
|
||||
isH5: false,
|
||||
h5Mode: 'register',
|
||||
h5LoginName: '',
|
||||
h5Nickname: '',
|
||||
h5Password: '',
|
||||
h5ConfirmPassword: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// #ifdef H5
|
||||
this.isH5 = true
|
||||
// #endif
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
console.log('AuthModal visible changed:', val)
|
||||
if (val) {
|
||||
this.avatarUrl = ''
|
||||
this.nickname = ''
|
||||
this.resetH5Form()
|
||||
setTimeout(() => {
|
||||
this.showAnimation = true
|
||||
}, 50)
|
||||
@@ -132,6 +206,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getStatusText() {
|
||||
if (this.isH5) {
|
||||
return this.h5Mode === 'register' ? 'H5 使用账号注册后自动登录' : 'H5 使用已注册账号登录'
|
||||
}
|
||||
if (!this.avatarUrl) {
|
||||
return '请选择您的微信头像开始授权'
|
||||
}
|
||||
@@ -151,6 +228,84 @@ export default {
|
||||
this.avatarUrl = ''
|
||||
this.nickname = ''
|
||||
},
|
||||
|
||||
resetH5Form() {
|
||||
this.h5Mode = 'register'
|
||||
this.h5LoginName = ''
|
||||
this.h5Nickname = ''
|
||||
this.h5Password = ''
|
||||
this.h5ConfirmPassword = ''
|
||||
},
|
||||
|
||||
setH5Mode(mode) {
|
||||
if (this.loading) return
|
||||
this.h5Mode = mode
|
||||
this.h5Password = ''
|
||||
this.h5ConfirmPassword = ''
|
||||
},
|
||||
|
||||
async handleH5Auth() {
|
||||
if (this.loading) return
|
||||
|
||||
if (!this.h5LoginName) {
|
||||
uni.showToast({ title: '请输入用户名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.h5Mode === 'register' && !this.h5Nickname) {
|
||||
uni.showToast({ title: '请输入昵称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.h5Password || this.h5Password.length < 6) {
|
||||
uni.showToast({ title: '密码至少6位', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.h5Mode === 'register' && this.h5Password !== this.h5ConfirmPassword) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const res = this.h5Mode === 'register'
|
||||
? await api.userApi.registerH5({
|
||||
loginName: this.h5LoginName,
|
||||
nickname: this.h5Nickname,
|
||||
password: this.h5Password
|
||||
})
|
||||
: await api.userApi.loginH5({
|
||||
loginName: this.h5LoginName,
|
||||
password: this.h5Password
|
||||
})
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.error || (this.h5Mode === 'register' ? '注册失败' : '登录失败'))
|
||||
}
|
||||
|
||||
authCheck.saveLogin(
|
||||
res.data,
|
||||
res.data.username || this.h5Nickname || this.h5LoginName,
|
||||
api.getBaseUrl()
|
||||
)
|
||||
|
||||
uni.showToast({
|
||||
title: this.h5Mode === 'register' ? '注册成功' : '登录成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
this.$emit('success', res.data)
|
||||
this.$emit('update:visible', false)
|
||||
} catch (err) {
|
||||
uni.showToast({
|
||||
title: err.message || (this.h5Mode === 'register' ? '注册失败' : '登录失败'),
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
this.$emit('error', err)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
handleUserInfoClick() {
|
||||
console.log('handleUserInfoClick', this.nickname)
|
||||
@@ -252,18 +407,7 @@ export default {
|
||||
if (res.success) {
|
||||
console.log('登录成功:', res.data)
|
||||
|
||||
uni.setStorageSync('userId', res.data.userId)
|
||||
uni.setStorageSync('userInfo', JSON.stringify(res.data))
|
||||
uni.setStorageSync('userCoins', res.data.coins || 0)
|
||||
uni.setStorageSync('vipLevel', res.data.vipLevel || 0)
|
||||
uni.setStorageSync('username', res.data.username || this.nickname)
|
||||
|
||||
let avatarUrl = res.data.avatarUrl || ''
|
||||
if (avatarUrl && !avatarUrl.startsWith('http') && !avatarUrl.startsWith('wxfile://')) {
|
||||
avatarUrl = api.getBaseUrl().replace('/api', '') + avatarUrl
|
||||
}
|
||||
uni.setStorageSync('userAvatar', avatarUrl)
|
||||
uni.setStorageSync('isLoggedIn', true)
|
||||
authCheck.saveLogin(res.data, this.nickname, api.getBaseUrl())
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
@@ -409,6 +553,7 @@ export default {
|
||||
padding: 0 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
@@ -417,6 +562,66 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.auth-mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #F3F4F6;
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #6B7280;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mode-item.active {
|
||||
background: #ffffff;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.h5-form-card {
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #F9FAFB;
|
||||
border: 2rpx solid #E5E7EB;
|
||||
}
|
||||
|
||||
.h5-input {
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
margin-bottom: 18rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 18rpx;
|
||||
background: #ffffff;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.h5-input:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.h5-form-tip {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.auth-tips {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>排盘工具</title>
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(safe-area-inset-top)') || CSS.supports('top: constant(safe-area-inset-top)'))
|
||||
document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -281,6 +281,14 @@
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/wechat-callback",
|
||||
"style": {
|
||||
"navigationBarTitleText": "微信登录中",
|
||||
"navigationBarBackgroundColor": "#07C160",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/share/view",
|
||||
"style": {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<view class="callback-page">
|
||||
<view class="callback-card">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="desc">{{ desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/utils/api.js'
|
||||
import authCheck from '@/utils/authCheck.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
title: '微信登录中',
|
||||
desc: '正在处理扫码授权结果...'
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
const params = this.resolveParams(options)
|
||||
|
||||
if (params.success !== '1') {
|
||||
const errorMessage = params.error || '微信登录失败,请重试'
|
||||
this.title = '登录失败'
|
||||
this.desc = errorMessage
|
||||
uni.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}, 1200)
|
||||
return
|
||||
}
|
||||
|
||||
const userData = {
|
||||
id: params.id || '',
|
||||
userId: params.userId || '',
|
||||
openId: params.openId || '',
|
||||
avatarUrl: params.avatarUrl || '',
|
||||
username: params.username || '微信用户',
|
||||
phone: params.phone || '',
|
||||
vipLevel: Number(params.vipLevel || 0),
|
||||
coins: Number(params.coins || 0),
|
||||
streakDays: Number(params.streakDays || 0),
|
||||
createTime: params.createTime || ''
|
||||
}
|
||||
|
||||
authCheck.saveLogin(userData, userData.username, api.getBaseUrl())
|
||||
this.title = '登录成功'
|
||||
this.desc = '正在返回首页...'
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}, 800)
|
||||
},
|
||||
methods: {
|
||||
resolveParams(options) {
|
||||
const params = { ...(options || {}) }
|
||||
|
||||
// #ifdef H5
|
||||
try {
|
||||
const url = window.location.href
|
||||
const queryIndex = url.indexOf('?')
|
||||
if (queryIndex >= 0) {
|
||||
const queryString = url.slice(queryIndex + 1)
|
||||
queryString.split('&').forEach((pair) => {
|
||||
if (!pair) return
|
||||
const [rawKey, rawValue = ''] = pair.split('=')
|
||||
const key = decodeURIComponent(rawKey || '')
|
||||
const value = decodeURIComponent(rawValue || '')
|
||||
if (key) {
|
||||
params[key] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析H5回调参数失败:', e)
|
||||
}
|
||||
// #endif
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.callback-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32rpx;
|
||||
background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.callback-card {
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 20rpx 60rpx rgba(7, 193, 96, 0.12);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
@@ -429,6 +429,24 @@ const userApi = {
|
||||
description: description
|
||||
}
|
||||
})
|
||||
},
|
||||
getWechatH5LoginUrl: (returnUrl) => {
|
||||
const query = returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ''
|
||||
return `${BASE_URL}/user/wechat/h5/start${query}`
|
||||
},
|
||||
registerH5: (data) => {
|
||||
return request({
|
||||
url: '/user/h5/register',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
loginH5: (data) => {
|
||||
return request({
|
||||
url: '/user/h5/login',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,31 @@ const authCheck = {
|
||||
const userInfo = this.getUserInfo()
|
||||
return userInfo ? userInfo.userId : null
|
||||
},
|
||||
|
||||
saveLogin(userData, fallbackUsername, baseUrl) {
|
||||
if (!userData) {
|
||||
return
|
||||
}
|
||||
|
||||
uni.setStorageSync('userId', userData.userId)
|
||||
uni.setStorageSync('userInfo', JSON.stringify(userData))
|
||||
uni.setStorageSync('userCoins', userData.coins || 0)
|
||||
uni.setStorageSync('vipLevel', userData.vipLevel || 0)
|
||||
uni.setStorageSync('username', userData.username || fallbackUsername || '微信用户')
|
||||
|
||||
let avatarUrl = userData.avatarUrl || ''
|
||||
if (
|
||||
baseUrl &&
|
||||
avatarUrl &&
|
||||
!avatarUrl.startsWith('http') &&
|
||||
!avatarUrl.startsWith('wxfile://')
|
||||
) {
|
||||
avatarUrl = baseUrl.replace('/api', '') + avatarUrl
|
||||
}
|
||||
|
||||
uni.setStorageSync('userAvatar', avatarUrl)
|
||||
uni.setStorageSync('isLoggedIn', true)
|
||||
},
|
||||
|
||||
logout() {
|
||||
uni.removeStorageSync('isLoggedIn')
|
||||
|
||||
@@ -3,5 +3,9 @@ import uni from '@dcloudio/vite-plugin-uni'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [uni()],
|
||||
root: 'src'
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
strictPort: true
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user