feat: 完善微信登录

This commit is contained in:
2026-05-27 00:12:45 +08:00
parent d1147713f8
commit 716a55744e
14 changed files with 160 additions and 256 deletions
+2 -2
View File
@@ -2,5 +2,5 @@
# H5 开发环境接口
VITE_H5_API_BASE_URL=http://localhost:3000
# 小程序开发环境接口
VITE_MP_API_BASE_URL=http://localhost:3000
# 小程序开发环境接口(真机调试使用局域网IP
VITE_MP_API_BASE_URL=http://192.168.0.111:3000
@@ -84,7 +84,7 @@ function handleTap(key: string) {
}
.tool-label {
font-size: $font-xs;
font-size: $font-base;
color: $text-secondary;
font-weight: $font-medium;
}
+1 -1
View File
@@ -6,7 +6,7 @@
"versionCode": "100",
"transformPx": false,
"mp-weixin": {
"appid": "",
"appid": "wx6b2d69c900f8f93a",
"setting": {
"urlCheck": false,
"es6": true,
@@ -1,16 +1,5 @@
<template>
<view class="page-cashbacks">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<view class="navbar-left" @tap="goBack">
<u-icon name="arrow-left" :size="20" color="#1A1A1A" />
</view>
<text class="navbar-title">返现明细</text>
<view class="navbar-right"></view>
</view>
</view>
<!-- 顶部统计卡片 -->
<view class="stats-header">
<view class="stats-main">
@@ -319,46 +308,6 @@ function goBack() {
flex-direction: column;
}
/* ========== 自定义导航栏 ========== */
.custom-navbar {
background: #ffffff;
padding: 0 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.navbar-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-left,
.navbar-right {
width: 80rpx;
display: flex;
align-items: center;
}
.navbar-left {
justify-content: flex-start;
padding: 12rpx;
margin-left: -12rpx;
transition: opacity 0.3s ease;
&:active {
opacity: 0.6;
}
}
.navbar-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
}
/* ========== 顶部统计卡片 ========== */
.stats-header {
margin: 24rpx 24rpx 16rpx;
-51
View File
@@ -1,16 +1,5 @@
<template>
<view class="page-records">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<view class="navbar-left" @tap="goBack">
<u-icon name="arrow-left" :size="20" color="#1A1A1A" />
</view>
<text class="navbar-title">我的邀请</text>
<view class="navbar-right"></view>
</view>
</view>
<!-- 顶部统计卡片 -->
<view class="stats-banner">
<view class="stats-item">
@@ -279,46 +268,6 @@ function goBack() {
flex-direction: column;
}
/* ========== 自定义导航栏 ========== */
.custom-navbar {
background: #ffffff;
padding: 0 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.navbar-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-left,
.navbar-right {
width: 80rpx;
display: flex;
align-items: center;
}
.navbar-left {
justify-content: flex-start;
padding: 12rpx;
margin-left: -12rpx;
transition: opacity 0.3s ease;
&:active {
opacity: 0.6;
}
}
.navbar-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
}
/* ========== 顶部统计横幅 ========== */
.stats-banner {
margin: 24rpx 24rpx 16rpx;
+113 -51
View File
@@ -22,7 +22,7 @@
<!-- 一键登录按钮微信获取手机号 -->
<button
v-if="isWechatMiniProgram"
v-if="isWechatMiniProgram && agreed"
class="quick-login-button"
open-type="getPhoneNumber"
@getphonenumber="handleQuickLogin"
@@ -31,6 +31,16 @@
一键登录
</button>
<!-- 未同意协议时显示的普通按钮 -->
<button
v-if="isWechatMiniProgram && !agreed"
class="quick-login-button"
@tap="handleQuickLoginCheck"
>
<u-icon name="phone-fill" :size="24" color="#fff" style="margin-right: 12rpx;" />
一键登录
</button>
<!-- 手机号安全登录按钮 -->
<button class="phone-login-button" @tap="handleShowPhoneModal">
<u-icon name="lock-fill" :size="24" color="#fff" style="margin-right: 12rpx;" />
@@ -76,7 +86,23 @@
</view>
<view class="modal-footer">
<button class="modal-button cancel" @tap="closeAgreementModal">取消</button>
<button class="modal-button confirm" @tap="confirmAgreement">确定</button>
<!-- 如果是微信一键登录触发的弹窗确定按钮带微信授权 -->
<button
v-if="isWechatMiniProgram && pendingLoginType === 'wechat'"
class="modal-button confirm"
open-type="getPhoneNumber"
@getphonenumber="handleQuickLoginAfterAgreement"
>
确定
</button>
<!-- 其他情况的普通确定按钮 -->
<button
v-else
class="modal-button confirm"
@tap="confirmAgreement"
>
确定
</button>
</view>
</view>
</view>
@@ -147,6 +173,7 @@ const agreed = ref(false);
const isWechatMiniProgram = ref(false);
const showAgreementModal = ref(false);
const showPhoneModal = ref(false);
const pendingLoginType = ref<'wechat' | 'phone' | null>(null);
const userStore = useUserStore();
let timer: ReturnType<typeof setInterval> | null = null;
@@ -176,6 +203,7 @@ function checkAgreement(action: () => void) {
function closeAgreementModal() {
showAgreementModal.value = false;
pendingAction = null;
pendingLoginType.value = null;
}
// 确认协议
@@ -186,54 +214,82 @@ function confirmAgreement() {
pendingAction();
pendingAction = null;
}
pendingLoginType.value = null;
}
// 一键登录检查协议
function handleQuickLoginCheck() {
if (!agreed.value) {
pendingLoginType.value = 'wechat';
showAgreementModal.value = true;
}
}
// 同意协议后的微信一键登录
async function handleQuickLoginAfterAgreement(e: any) {
agreed.value = true;
showAgreementModal.value = false;
pendingLoginType.value = null;
await handleQuickLogin(e);
}
// 一键登录(微信获取手机号)
async function handleQuickLogin(e: any) {
const action = async () => {
try {
const { code, encryptedData, iv } = e.detail;
try {
const { code: phoneCode, encryptedData, iv } = e.detail;
if (!code) {
uni.showToast({ title: '获取手机号失败', icon: 'none' });
return;
}
console.log('微信手机号授权返回:', { phoneCode, encryptedData, iv });
uni.showLoading({ title: '登录中...' });
// 获取待绑定的邀请码
const inviteCode = uni.getStorageSync('pendingInviteCode');
// 调用后端接口,使用微信手机号登录
const res = await loginByWechatPhone(code, encryptedData, iv, inviteCode);
// 登录成功后清除邀请码
if (inviteCode) {
uni.removeStorageSync('pendingInviteCode');
}
userStore.setLogin(res.data);
uni.hideLoading();
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' });
}, 500);
} catch (error: any) {
uni.hideLoading();
console.error('一键登录失败', error);
uni.showToast({
title: error.message || '登录失败,请重试',
icon: 'none'
});
if (!phoneCode) {
uni.showToast({ title: '获取手机号失败', icon: 'none' });
return;
}
};
if (!checkAgreement(action)) {
return;
uni.showLoading({ title: '登录中...' });
// 先调用 wx.login 获取登录 code
const loginRes = await new Promise<any>((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject,
});
});
if (!loginRes.code) {
uni.hideLoading();
uni.showToast({ title: '获取登录凭证失败', icon: 'none' });
return;
}
console.log('wx.login 返回的 code:', loginRes.code);
// 获取待绑定的邀请码
const inviteCode = uni.getStorageSync('pendingInviteCode');
// 调用后端接口,使用微信手机号登录
const res = await loginByWechatPhone(loginRes.code, encryptedData, iv, inviteCode);
// 登录成功后清除邀请码
if (inviteCode) {
uni.removeStorageSync('pendingInviteCode');
}
userStore.setLogin(res.data);
uni.hideLoading();
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' });
}, 500);
} catch (error: any) {
uni.hideLoading();
console.error('一键登录失败', error);
uni.showToast({
title: error.message || '登录失败,请重试',
icon: 'none'
});
}
await action();
}
// 显示手机号登录弹窗
@@ -243,6 +299,7 @@ function handleShowPhoneModal() {
};
if (!checkAgreement(action)) {
pendingLoginType.value = 'phone';
return;
}
@@ -630,32 +687,37 @@ async function handlePhoneLogin() {
.modal-footer {
display: flex;
border-top: 1rpx solid #f0f0f0;
gap: 24rpx;
padding: 0 32rpx 32rpx;
.modal-button {
flex: 1;
height: 96rpx;
line-height: 96rpx;
height: 88rpx;
line-height: 88rpx;
font-size: 30rpx;
font-weight: 600;
border: none;
background: transparent;
transition: all 0.2s;
border-radius: 16rpx;
transition: all 0.3s;
&.cancel {
color: #999;
border-right: 1rpx solid #f0f0f0;
background: #f7f8fa;
color: #666;
&:active {
background: #f7f8fa;
background: #e8e9eb;
transform: scale(0.98);
}
}
&.confirm {
color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
&:active {
background: rgba(102, 126, 234, 0.1);
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.4);
transform: scale(0.98);
}
}
}
-5
View File
@@ -19,11 +19,6 @@
/>
</view>
<!-- 商家服务 -->
<view class="menu-section">
<menu-list
-12
View File
@@ -3,9 +3,6 @@
<!-- 搜索头部 -->
<view class="search-header">
<view class="search-bar">
<view class="back-icon" @tap="goBack">
<u-icon name="arrow-left" size="20" color="#333" />
</view>
<view class="input-wrapper">
<u-icon name="search" size="18" color="#999" />
<input
@@ -238,15 +235,6 @@ function formatMerchant(merchant: any) {
margin-bottom: 16rpx;
}
.back-icon {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.input-wrapper {
flex: 1;
display: flex;
@@ -1,14 +1,5 @@
<template>
<view class="page-withdrawals">
<!-- 导航栏 -->
<view class="navbar">
<view class="navbar-left" @tap="goBack">
<u-icon name="arrow-left" :size="20" color="#1A1A1A" />
</view>
<text class="navbar-title">提现记录</text>
<view class="navbar-right"></view>
</view>
<!-- 筛选栏 -->
<view class="filter-bar">
<view
@@ -212,43 +203,6 @@ function goBack() {
flex-direction: column;
}
/* 导航栏 */
.navbar {
background: #ffffff;
padding: 0 24rpx;
border-bottom: 1rpx solid #F0F0F0;
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
}
.navbar-left,
.navbar-right {
width: 80rpx;
display: flex;
align-items: center;
}
.navbar-left {
justify-content: flex-start;
padding: 12rpx;
margin-left: -12rpx;
transition: opacity 0.3s ease;
&:active {
opacity: 0.6;
}
}
.navbar-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
}
/* 筛选栏 */
.filter-bar {
background: #ffffff;
+6 -7
View File
@@ -2,21 +2,17 @@
function getBaseURL(): string {
// 判断当前运行平台
// #ifdef H5
return import.meta.env.VITE_H5_API_BASE_URL || 'http://localhost:3000';
return 'http://localhost:3000';
// #endif
// #ifdef MP-WEIXIN
return import.meta.env.VITE_MP_API_BASE_URL || 'http://localhost:3000';
return 'http://192.168.0.111:3000';
// #endif
// 其他平台默认值
// #ifndef H5 || MP-WEIXIN
return import.meta.env.VITE_MP_API_BASE_URL || 'http://localhost:3000';
// #endif
return 'http://192.168.0.111:3000';
}
const BASE_URL = getBaseURL();
interface RequestOptions {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -54,6 +50,9 @@ export function request<T = any>(options: RequestOptions): Promise<ApiResponse<T
header['Authorization'] = `Bearer ${token}`;
}
// 动态获取 BASE_URL
const BASE_URL = getBaseURL();
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}${url}`,
+2 -1
View File
@@ -5,6 +5,7 @@ import { ScheduleModule } from '@nestjs/schedule';
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import redisConfig from './config/redis.config';
import wechatConfig from './config/wechat.config';
import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module';
// 新的模块结构 - 按端分类
@@ -20,7 +21,7 @@ import { WebsiteModule } from './modules/website/website.module';
TaskScheduleModule,
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, jwtConfig, redisConfig],
load: [databaseConfig, jwtConfig, redisConfig, wechatConfig],
envFilePath: ['.env.local', '.env'],
}),
TypeOrmModule.forRootAsync({
+6
View File
@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('wechat', () => ({
appId: process.env.WECHAT_APPID,
appSecret: process.env.WECHAT_SECRET,
}));
@@ -239,18 +239,7 @@ export class AuthService {
session_key: string;
unionid?: string;
}> {
// 开发模式:返回模拟数据
const isDev = process.env.NODE_ENV === 'development';
if (isDev) {
this.logger.log(`[DEV] 模拟微信登录: code=${code}`);
return {
openid: `dev_openid_${Date.now()}`,
session_key: 'dev_session_key_1234567890abcdef',
unionid: `dev_unionid_${Date.now()}`,
};
}
// 生产模式:调用微信API
// 调用微信API
const appId = this.configService.get<string>('wechat.appId');
const appSecret = this.configService.get<string>('wechat.appSecret');
@@ -261,12 +250,17 @@ export class AuthService {
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`;
try {
this.logger.log(`调用微信API: appId=${appId}, code=${code.substring(0, 10)}...`);
const response = await fetch(url);
const data = await response.json();
this.logger.log(`微信API响应: ${JSON.stringify(data)}`);
if (data.errcode) {
this.logger.error(`微信API错误: ${data.errmsg}`);
throw new BadRequestException('微信授权失败');
const errorMsg = this.getWechatErrorMessage(data.errcode, data.errmsg);
this.logger.error(`微信API错误 [${data.errcode}]: ${errorMsg}`);
this.logger.error(`微信API错误 data : ${data}`);
throw new BadRequestException(errorMsg);
}
return {
@@ -275,29 +269,35 @@ export class AuthService {
unionid: data.unionid,
};
} catch (error) {
if (error instanceof BadRequestException) {
throw error;
}
this.logger.error('调用微信API失败', error);
throw new BadRequestException('微信授权失败');
throw new BadRequestException('微信授权失败,请重试');
}
}
private getWechatErrorMessage(errcode: number, errmsg: string): string {
const errorMap: Record<number, string> = {
40029: '微信登录code无效,请重新授权',
45011: '微信登录频率限制,请稍后再试',
40163: 'code已被使用,请重新授权',
};
if (errcode === -1) {
return '微信系统繁忙,请稍后再试';
}
return errorMap[errcode] || `微信授权失败: ${errmsg}`;
}
private decryptWechatData(
encryptedData: string,
iv: string,
sessionKey: string,
): any {
try {
// 开发模式:返回模拟数据
const isDev = process.env.NODE_ENV === 'development';
if (isDev) {
this.logger.log('[DEV] 模拟解密微信手机号');
return {
phoneNumber: '13800138000',
purePhoneNumber: '13800138000',
countryCode: '86',
};
}
// 生产模式:解密数据
// 解密数据
const sessionKeyBuffer = Buffer.from(sessionKey, 'base64');
const encryptedDataBuffer = Buffer.from(encryptedData, 'base64');
const ivBuffer = Buffer.from(iv, 'base64');