feat: add h5 account auth flow

This commit is contained in:
H1d3rOne committed 2026-03-24 11:55:29 +08:00
1 parent ef6346a563
commit e4e3f0fdb3
13 files changed
+774 -51

No files matched your search

+271 -15
View File
@@ -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";
}
}
+12
View File
@@ -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);
}
}
}
+8 -3
View File
@@ -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}
+12
View File
@@ -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;
}
-17
View File
@@ -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>
+8
View File
@@ -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>
+18
View File
@@ -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
})
}
}
+25
View File
@@ -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')
+5 -1
View File
@@ -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
}
})