服务号授权

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_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876 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_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876 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": [ "pages": [
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "授权登录"
}
},
{ {
"path": "pages/login/redirect-page", "path": "pages/login/redirect-page",
"style": { "style": {
@ -206,12 +212,6 @@
"navigationBarTitleText": "上传证照" "navigationBarTitleText": "上传证照"
} }
}, },
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "授权登录"
}
},
{ {
"path": "pages/work/team/invite/invite-patient", "path": "pages/work/team/invite/invite-patient",
"style": { "style": {

View File

@ -1,25 +1,31 @@
<template> <template>
<view class="pt-lg px-15 flex flex-col items-center text-center"> <view class="login-page">
<image src="/static/logo-plain.png" class="logo"></image> <view class="pt-lg px-15 flex flex-col items-center text-center">
<view class="mt-15 text-xl font-semibold text-dark">柚健康</view> <image src="/static/logo-plain.png" class="logo"></image>
<view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view> <view class="mt-15 text-xl font-semibold text-dark">柚健康</view>
</view> <view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view>
<view class="login-btn-wrap"> </view>
<!-- <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber"> <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-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber"
</button> @getphonenumber="getPhoneNumber">
<button v-else class="login-btn" type="primary" @click="remind()"> 手机号快捷登录
手机号快捷登录 </button>
</button> <button v-else class="login-btn" type="primary" @click="remind()">
</view> 手机号快捷登录
<view class="flex items-center justify-center mt-12 px-15" @click="checked = !checked"> </button>
<checkbox :checked="checked" style="transform: scale(0.7)" /> </view>
<view class="text-sm text-gray">我已阅读并同意</view> <view class="mt-10 text-center">
<view class="text-sm text-primary">用户协议</view> <text class="mp-oauth-link" @click="toMpOauth">关联公众号用于用户映射</text>
<view class="text-sm text-primary">隐私政策</view> </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> </view>
</template> </template>
@ -28,7 +34,7 @@ import { ref } from "vue";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { get } from "@/utils/cache"; import { get, set } from "@/utils/cache";
import { toast } from "@/utils/widget"; import { toast } from "@/utils/widget";
const team = ref(true); 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) { async function getPhoneNumber(e) {
console.log('e', e);
const phoneCode = e && e.detail && e.detail.code; const phoneCode = e && e.detail && e.detail.code;
console.log('phoneCode', phoneCode);
// if (e && !phoneCode) return; // if (e && !phoneCode) return;
const res = await login(phoneCode); const res = await login(phoneCode);
@ -92,6 +120,11 @@ async function attempToPage(url) {
} }
onLoad((opts) => { 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") { if (opts.source === "teamInvite") {
team.value = get("invite-team-info"); team.value = get("invite-team-info");
redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`; redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
@ -142,4 +175,9 @@ onLoad((opts) => {
.login-btn:active { .login-btn:active {
background: linear-gradient(270deg, #1b5cc8 2.26%, #0877f1 94.33%); background: linear-gradient(270deg, #1b5cc8 2.26%, #0877f1 94.33%);
} }
.mp-oauth-link {
color: #0877f1;
font-size: 26rpx;
}
</style> </style>

View File

@ -1,19 +1,41 @@
<template> <template>
<view class="webview-container"> <view class="webview-container">
<web-view :src="url"></web-view> <web-view :src="url" @message="onMessage"></web-view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import { ref } from "vue"; import { ref } from "vue";
import { set } from "@/utils/cache";
import { toast } from "@/utils/widget";
const url = ref(""); const url = ref("");
const purpose = ref("");
onLoad((options) => { onLoad((options) => {
if (options.url) { if (options.url) {
url.value = decodeURIComponent(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> </script>
<style scoped> <style scoped>

View File

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