服务号授权

This commit is contained in:
zhanchao 2026-01-29 17:39:42 +08:00
parent 2cf940e0b9
commit b21609d1d8
7 changed files with 192 additions and 31 deletions

View File

@ -4,3 +4,4 @@ MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876
MP_WX_MP_APP_ID=wxce46d19ff09c1832

View File

@ -4,3 +4,5 @@ MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876
MP_WX_MP_APP_ID=wxce46d19ff09c1832
MP_WX_MP_OAUTH_URL=www.youcan365.com

View File

@ -1,5 +1,11 @@
{
"pages": [
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "授权登录"
}
},
{
"path": "pages/login/redirect-page",
"style": {
@ -206,12 +212,6 @@
"navigationBarTitleText": "上传证照"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "授权登录"
}
},
{
"path": "pages/work/team/invite/invite-patient",
"style": {

View File

@ -1,25 +1,31 @@
<template>
<view class="pt-lg px-15 flex flex-col items-center text-center">
<image src="/static/logo-plain.png" class="logo"></image>
<view class="mt-15 text-xl font-semibold text-dark">柚健康</view>
<view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view>
</view>
<view class="login-btn-wrap">
<!-- <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
手机号快捷登录
</button> -->
<button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
手机号快捷登录
</button>
<button v-else class="login-btn" type="primary" @click="remind()">
手机号快捷登录
</button>
</view>
<view class="flex items-center justify-center mt-12 px-15" @click="checked = !checked">
<checkbox :checked="checked" style="transform: scale(0.7)" />
<view class="text-sm text-gray">我已阅读并同意</view>
<view class="text-sm text-primary">用户协议</view>
<view class="text-sm text-primary">隐私政策</view>
<view class="login-page">
<view class="pt-lg px-15 flex flex-col items-center text-center">
<image src="/static/logo-plain.png" class="logo"></image>
<view class="mt-15 text-xl font-semibold text-dark">柚健康</view>
<view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view>
</view>
<view class="login-btn-wrap">
<!-- <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
手机号快捷登录
</button> -->
<button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber">
手机号快捷登录
</button>
<button v-else class="login-btn" type="primary" @click="remind()">
手机号快捷登录
</button>
</view>
<view class="mt-10 text-center">
<text class="mp-oauth-link" @click="toMpOauth">关联公众号用于用户映射</text>
</view>
<view class="flex items-center justify-center mt-12 px-15" @click="checked = !checked">
<checkbox :checked="checked" style="transform: scale(0.7)" />
<view class="text-sm text-gray">我已阅读并同意</view>
<view class="text-sm text-primary">用户协议</view>
<view class="text-sm text-primary">隐私政策</view>
</view>
</view>
</template>
@ -28,7 +34,7 @@ import { ref } from "vue";
import { storeToRefs } from "pinia";
import { onLoad } from "@dcloudio/uni-app";
import useAccountStore from "@/store/account";
import { get } from "@/utils/cache";
import { get, set } from "@/utils/cache";
import { toast } from "@/utils/widget";
const team = ref(true);
@ -67,8 +73,30 @@ function toHome() {
});
}
function toMpOauth() {
// snsapi_base
// TODO: YOUR_MP_APPID appid
// redirect_uri www.youcan365.com
const MP_APPID = __VITE_ENV__.MP_WX_MP_APP_ID;
const REDIRECT_URI = "https://www.youcan365.com/wx-callback";
const state = `ykt_wxapp_${Date.now()}`;
const oauthUrl =
`https://open.weixin.qq.com/connect/oauth2/authorize` +
`?appid=${MP_APPID}` +
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
`&response_type=code` +
`&scope=snsapi_base` +
`&state=${encodeURIComponent(state)}` +
`#wechat_redirect`;
uni.navigateTo({
url: `/pages/webview/webview?purpose=mp_oauth&url=${encodeURIComponent(oauthUrl)}`,
});
}
async function getPhoneNumber(e) {
console.log('e', e);
const phoneCode = e && e.detail && e.detail.code;
console.log('phoneCode', phoneCode);
// if (e && !phoneCode) return;
const res = await login(phoneCode);
@ -92,6 +120,11 @@ async function attempToPage(url) {
}
onLoad((opts) => {
// H5 OAuth oauth code ?mpCode=xxx
// store/account.js
if (opts && opts.mpCode) {
set("mp-oauth-code", opts.mpCode, 300); // 5
}
if (opts.source === "teamInvite") {
team.value = get("invite-team-info");
redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
@ -142,4 +175,9 @@ onLoad((opts) => {
.login-btn:active {
background: linear-gradient(270deg, #1b5cc8 2.26%, #0877f1 94.33%);
}
.mp-oauth-link {
color: #0877f1;
font-size: 26rpx;
}
</style>

View File

@ -1,19 +1,41 @@
<template>
<view class="webview-container">
<web-view :src="url"></web-view>
<web-view :src="url" @message="onMessage"></web-view>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { set } from "@/utils/cache";
import { toast } from "@/utils/widget";
const url = ref("");
const purpose = ref("");
onLoad((options) => {
if (options.url) {
url.value = decodeURIComponent(options.url);
console.log('url2', url.value);
}
purpose.value = options.purpose || "";
});
function onMessage(e) {
// H5(OAuth) wx.miniProgram.postMessage mpCode
// wx.miniProgram.postMessage({ data: { mpCode: '<code>' } })
const msg = e && e.detail && e.detail.data;
const payload = Array.isArray(msg) ? msg[msg.length - 1] : msg;
const mpCode = payload && payload.mpCode;
if (!mpCode) return;
console.log('mpCode', mpCode);
set("mp-oauth-code", mpCode, 300); // 5
toast("公众号授权成功");
//
setTimeout(() => {
uni.navigateTo({ url: '/pages/login/login' });
}, 300);
}
</script>
<style scoped>

View File

@ -3,12 +3,14 @@ import { defineStore } from "pinia";
import api from '@/utils/api';
import { toast } from '@/utils/widget';
import { initGlobalTIM, globalTimChatManager } from "@/utils/tim-chat.js";
import { get, remove } from "@/utils/cache";
const env = __VITE_ENV__;
export default defineStore("accountStore", () => {
const appid = env.MP_WX_APP_ID;
const corpId = env.MP_CORP_ID;
const mp_appid = env.MP_WX_MP_APP_ID;
const account = ref(null);
const loading = ref(false);
const loginPromise = ref(null);
@ -32,17 +34,30 @@ export default defineStore("accountStore", () => {
async function loginByCode(phoneCode = '') {
try {
console.log('appid', appid);
console.log('mp_appid', mp_appid);
// 获取小程序 code静默无需授权页
const { code } = await uni.login({
appid,
provider: "weixin",
scope: "snsapi_base",
});
// 公众号服务号OAuth code 不能在小程序内通过 uni.login 获取。
// 正确方式:在“微信内置浏览器/H5”走公众号 OAuth 重定向后,把 code 回传到小程序(例如通过 query 参数/缓存),这里读取并透传给后端做映射。
const mpCode = get("mp-oauth-code", "");
if (code) {
// 将小程序 code + 公众号 code 一起传给后端,进行用户映射(后端用各自 appid/secret 换 openid/unionid
const res = await api('wxAppLogin', {
phoneCode,
code,
code, // 小程序code
mpCode, // 公众号 code如果有
corpId,
});
if (mpCode) remove("mp-oauth-code");
if (res.success && res.data) {
if (!res.data.mobile) {
const target = '/pages/login/login';
@ -52,7 +67,6 @@ export default defineStore("accountStore", () => {
account.value = res.data;
openid.value = res.data.openid;
// 登录成功后初始化腾讯IM
await getDoctorInfo(openid.value);
await initIMAfterLogin();
return res.data
@ -61,6 +75,7 @@ export default defineStore("accountStore", () => {
toast('登录失败,请重新登录');
} catch (e) {
console.error('登录失败:', e);
toast('登录失败,请重新登录');
}
return Promise.reject()

83
wx-callback.html Normal file
View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信授权回调</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
padding: 20px;
color: #333;
}
.url-display {
word-break: break-all;
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 14px;
}
.status {
font-weight: bold;
}
</style>
</head>
<body>
<h3>微信公众号授权回调页</h3>
<p>当前页面地址:</p>
<div id="url-display" class="url-display"></div>
<p>状态:</p>
<div id="status" class="status">正在处理...</div>
<script>
(function () {
const urlDisplay = document.getElementById('url-display');
const statusDiv = document.getElementById('status');
// 显示当前地址
urlDisplay.textContent = location.href;
// 解析 code
const params = new URLSearchParams(location.search);
const code = params.get('code');
if (!code) {
statusDiv.textContent = '错误URL中未找到 code 参数。';
return;
}
statusDiv.textContent = '成功获取到 code等待小程序环境注入...';
function sendCodeToMiniProgram() {
if (!wx || !wx.miniProgram) {
statusDiv.textContent = '错误:当前不在小程序 web-view 环境中,无法回传 code。';
return;
}
statusDiv.textContent = '检测到小程序环境,正在回传 code...';
wx.miniProgram.postMessage({ data: { mpCode: code } });
setTimeout(function () {
wx.miniProgram.navigateBack();
}, 200);
}
// 等 JS-SDK/WeixinJSBridge 准备好再发
if (typeof WeixinJSBridge !== 'undefined') {
sendCodeToMiniProgram();
} else {
document.addEventListener('WeixinJSBridgeReady', sendCodeToMiniProgram, false);
}
})();
</script>
</body>
</html>