@@ -0,0 +1,36 @@
|
||||
import { weixinSdk } from './weixin';
|
||||
|
||||
export type ClientGpsLocation = {
|
||||
province?: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address?: string;
|
||||
};
|
||||
|
||||
/** 尝试获取客户端 GPS(微信 JSSDK / 浏览器 Geolocation),失败返回 null 不阻塞下单 */
|
||||
export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | null> {
|
||||
if (process.env.TARO_ENV === 'weapp') {
|
||||
try {
|
||||
const Taro = (await import('@tarojs/taro')).default;
|
||||
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
|
||||
Taro.getLocation({
|
||||
type: 'gcj02',
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
return { latitude: loc.latitude, longitude: loc.longitude };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const loc = await weixinSdk.getLocation();
|
||||
if (!loc) return null;
|
||||
return {
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { goLogin } from './auth-nav';
|
||||
import { isLoggedIn } from './api';
|
||||
import { ensureWechatAuthForPay } from './wechat-auth';
|
||||
import { fetchClientConfig, fetchUserProfile, needsWechatAuthForPay } from './pay-wechat';
|
||||
|
||||
/** 支付前门禁:未登录/未验手机/未绑微信时跳转登录页 */
|
||||
/**
|
||||
* 支付前门禁:
|
||||
* - 未登录 / 未验手机 → 跳转登录页
|
||||
* - H5 微信内缺 openId → 尝试 OAuth(可能跳转微信授权页)
|
||||
* - 小程序缺绑定 → 跳转登录页 needWechat
|
||||
*/
|
||||
export async function ensurePayReady(returnPath: string): Promise<boolean> {
|
||||
if (!isLoggedIn()) {
|
||||
goLogin(returnPath);
|
||||
@@ -15,11 +21,23 @@ export async function ensurePayReady(returnPath: string): Promise<boolean> {
|
||||
goLogin(returnPath, { needPhone: '1' });
|
||||
return false;
|
||||
}
|
||||
if (needsWechatAuthForPay(config, profile)) {
|
||||
goLogin(returnPath, { needWechat: '1' });
|
||||
if (!needsWechatAuthForPay(config, profile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (process.env.TARO_ENV === 'h5') {
|
||||
const auth = await ensureWechatAuthForPay();
|
||||
if (auth.ok) return true;
|
||||
if ('needBindPhone' in auth && auth.needBindPhone) {
|
||||
goLogin(returnPath, { bindMode: '1', wxSessionKey: auth.wxSessionKey });
|
||||
return false;
|
||||
}
|
||||
// redirecting:正在跳转微信 OAuth
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
goLogin(returnPath, { needWechat: '1' });
|
||||
return false;
|
||||
} catch {
|
||||
goLogin(returnPath);
|
||||
return false;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { ClientRuntimeConfig, WechatJsapiPrepayParams, WechatLoginResult, WechatPayOrderResult } from '@dukang/shared-types';
|
||||
import type {
|
||||
ClientRuntimeConfig,
|
||||
WechatJsapiPrepayParams,
|
||||
WechatLoginResult,
|
||||
WechatPayOrderResult,
|
||||
} from '@dukang/shared-types';
|
||||
import { WECHAT_AUTH_REQUIRED, isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||
import { invokeWechatPay } from '@dukang/weixin-sdk';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { request, type UserProfile } from './api';
|
||||
import { syncMiniWechatProfile } from './mini-wechat-profile';
|
||||
import { request, saveAuth, type UserProfile } from './api';
|
||||
import { isWechatEnv, weixinSdk } from './weixin';
|
||||
|
||||
export function isMiniWechatEnv(): boolean {
|
||||
return process.env.TARO_ENV === 'weapp';
|
||||
@@ -21,13 +25,35 @@ export async function fetchUserProfile(): Promise<UserProfile> {
|
||||
return request<UserProfile>('/auth/me');
|
||||
}
|
||||
|
||||
/** 真实微信支付且未绑定微信时需要授权 */
|
||||
/** 真实微信支付且未绑定微信时需要授权(小程序 / H5 微信内) */
|
||||
export function needsWechatAuthForPay(
|
||||
config: ClientRuntimeConfig,
|
||||
profile: UserProfile | null,
|
||||
): boolean {
|
||||
if (!isWxAuthorizeEnabled(config)) return false;
|
||||
return !config.mockPay && config.wechatPayEnabled && isMiniWechatEnv() && !profile?.hasWechat;
|
||||
return !config.mockPay && config.wechatPayEnabled && isWechatEnv() && !profile?.hasWechat;
|
||||
}
|
||||
|
||||
export function saveWechatLoginResult(result: WechatLoginResult): boolean {
|
||||
if (!result.accessToken) return false;
|
||||
saveAuth({
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** H5:发起公众号 OAuth(可能直接跳转);小程序请用 bindWechatForUser */
|
||||
export async function authorizeWechatForPay(): Promise<WechatLoginResult | void> {
|
||||
const config = await fetchClientConfig();
|
||||
if (!isWxAuthorizeEnabled(config)) return;
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error('请在微信内打开以完成授权');
|
||||
}
|
||||
if (process.env.TARO_ENV === 'h5') {
|
||||
return weixinSdk.login();
|
||||
}
|
||||
throw new Error('请使用小程序微信授权');
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
@@ -49,9 +75,12 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> {
|
||||
});
|
||||
|
||||
if (result.mode === 'jsapi' && result.prepay) {
|
||||
await invokeWechatPay(result.prepay as WechatJsapiPrepayParams, {
|
||||
platform: isMiniWechatEnv() ? 'mini' : undefined,
|
||||
});
|
||||
const prepay = result.prepay as WechatJsapiPrepayParams;
|
||||
if (process.env.TARO_ENV === 'h5') {
|
||||
await weixinSdk.pay(prepay);
|
||||
} else {
|
||||
await invokeWechatPay(prepay, { platform: 'mini' });
|
||||
}
|
||||
const paid = await waitOrderPaid(orderId);
|
||||
return paid ? 'paid' : 'pending';
|
||||
}
|
||||
@@ -61,23 +90,5 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> {
|
||||
|
||||
export type WechatBindResult =
|
||||
| { ok: true; profile?: UserProfile }
|
||||
| { ok: false; needBindPhone: true; wxSessionKey: string };
|
||||
|
||||
export async function bindWechatForUser(
|
||||
prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null,
|
||||
): Promise<WechatBindResult> {
|
||||
const res = await Taro.login();
|
||||
if (!res.code) {
|
||||
throw new Error(res.errMsg || '微信授权失败');
|
||||
}
|
||||
const data = await request<WechatLoginResult>('/auth/wechat/bind', {
|
||||
method: 'POST',
|
||||
data: { code: res.code, platform: 'mini' },
|
||||
});
|
||||
if (data.needBindPhone && data.wxSessionKey) {
|
||||
return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
|
||||
}
|
||||
await syncMiniWechatProfile(prefetchedWxProfile);
|
||||
const profile = await fetchUserProfile();
|
||||
return { ok: true, profile };
|
||||
}
|
||||
| { ok: false; needBindPhone: true; wxSessionKey: string }
|
||||
| { ok: false; redirecting: true };
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { request } from './api';
|
||||
import { getWechatLocationDetailed } from '@dukang/weixin-sdk';
|
||||
import { API_BASE, getToken, request } from './api';
|
||||
import { DEFAULT_REGION, regionFromGeo, type RegionSelection } from './region-data';
|
||||
import { FALLBACK_CITY_CODE } from './product-images';
|
||||
import { ClientApp } from '@dukang/shared-types';
|
||||
|
||||
export const GPS_CITY_STORAGE_KEY = 'dukang_gps_city';
|
||||
|
||||
@@ -108,7 +110,7 @@ async function promptLocationAuth() {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function getLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Taro.getLocation({
|
||||
type: 'gcj02',
|
||||
@@ -118,6 +120,37 @@ function getLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveViaH5Jssdk(): Promise<ResolvedUserCity | null> {
|
||||
const outcome = await getWechatLocationDetailed({
|
||||
apiBase: API_BASE,
|
||||
clientApp: ClientApp.USER_H5,
|
||||
getAccessToken: () => getToken() || null,
|
||||
});
|
||||
|
||||
if (!outcome.location) {
|
||||
await reportLocationToServer({
|
||||
sdk: outcome.sdk,
|
||||
status: 'fail',
|
||||
errMsg: outcome.errMsg,
|
||||
}).catch(() => {});
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await reportLocationToServer({
|
||||
latitude: outcome.location.latitude,
|
||||
longitude: outcome.location.longitude,
|
||||
sdk: outcome.sdk,
|
||||
status: 'success',
|
||||
});
|
||||
const resolved = toResolved(data);
|
||||
if (resolved) writeCache(resolved);
|
||||
return resolved;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取并解析用户当前城市;失败返回郑州市兜底 */
|
||||
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
|
||||
if (!force) {
|
||||
@@ -125,12 +158,17 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
if (process.env.TARO_ENV === 'h5') {
|
||||
const fromJssdk = await resolveViaH5Jssdk();
|
||||
return fromJssdk ?? FALLBACK_CITY;
|
||||
}
|
||||
|
||||
if (process.env.TARO_ENV !== 'weapp') {
|
||||
return FALLBACK_CITY;
|
||||
}
|
||||
|
||||
try {
|
||||
const loc = await getLocation();
|
||||
const loc = await getMiniLocation();
|
||||
const data = await reportLocationToServer({
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
|
||||
@@ -1,17 +1,124 @@
|
||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
||||
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { request } from './api';
|
||||
import { saveAuth } from './api';
|
||||
import { syncMiniWechatProfile } from './mini-wechat-profile';
|
||||
import {
|
||||
authorizeWechatForPay,
|
||||
fetchClientConfig,
|
||||
fetchUserProfile,
|
||||
needsWechatAuthForPay,
|
||||
saveWechatLoginResult,
|
||||
type WechatBindResult,
|
||||
} from './pay-wechat';
|
||||
import { isWechatEnv, weixinSdk } from './weixin';
|
||||
|
||||
/** 小程序微信授权登录:Taro.login → /auth/login/wechat(资料上报由调用方 saveAuth 后执行) */
|
||||
export async function loginWithWechat(): Promise<WechatLoginResult> {
|
||||
export type WechatAuthEnsureResult =
|
||||
| { ok: true }
|
||||
| { ok: false; redirecting: true }
|
||||
| { ok: false; needBindPhone: true; wxSessionKey: string };
|
||||
|
||||
/** 小程序:Taro.login → /auth/login/wechat;H5:公众号 OAuth */
|
||||
export async function loginWithWechat(): Promise<WechatLoginResult | void> {
|
||||
if (process.env.TARO_ENV === 'h5') {
|
||||
return loginWithWechatSdk();
|
||||
}
|
||||
const res = await Taro.login();
|
||||
if (!res.code) {
|
||||
throw new Error(res.errMsg || '微信登录失败,未获取到 code');
|
||||
}
|
||||
const { request } = await import('./api');
|
||||
return request<WechatLoginResult>('/auth/login/wechat', {
|
||||
method: 'POST',
|
||||
data: { code: res.code, platform: 'mini' },
|
||||
});
|
||||
}
|
||||
|
||||
export { bindWechatForUser } from './pay-wechat';
|
||||
export async function checkNeedsWechatAuth(): Promise<boolean> {
|
||||
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
|
||||
return needsWechatAuthForPay(config, profile);
|
||||
}
|
||||
|
||||
/** 真实微信支付前确保已绑定微信;OAuth 跳转时返回 redirecting */
|
||||
export async function ensureWechatAuthForPay(): Promise<WechatAuthEnsureResult> {
|
||||
if (!isWechatEnv()) return { ok: true };
|
||||
if (!(await checkNeedsWechatAuth())) return { ok: true };
|
||||
|
||||
const result = await authorizeWechatForPay();
|
||||
if (!result) return { ok: false, redirecting: true };
|
||||
|
||||
if (result.needBindPhone && result.wxSessionKey) {
|
||||
return { ok: false, needBindPhone: true, wxSessionKey: result.wxSessionKey };
|
||||
}
|
||||
|
||||
if (saveWechatLoginResult(result)) {
|
||||
return { ok: true };
|
||||
}
|
||||
return { ok: false, redirecting: true };
|
||||
}
|
||||
|
||||
/** 处理 URL 中 OAuth ?code= 回调(H5 公众号) */
|
||||
export async function handleWechatAuthCallback(): Promise<WechatLoginResult | null> {
|
||||
if (process.env.TARO_ENV !== 'h5') return null;
|
||||
if (!isWechatEnv()) return null;
|
||||
const config = await fetchClientConfig();
|
||||
if (!isWxAuthorizeEnabled(config)) return null;
|
||||
const result = await weixinSdk.handleOAuthCallback();
|
||||
if (result) stripOAuthParamsFromLocation();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function loginWithWechatSdk(): Promise<WechatLoginResult | void> {
|
||||
const config = await fetchClientConfig();
|
||||
if (!isWxAuthorizeEnabled(config)) return;
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error('请在微信内打开以使用微信一键授权');
|
||||
}
|
||||
return weixinSdk.login();
|
||||
}
|
||||
|
||||
export function applyWechatLoginResult(result: WechatLoginResult): boolean {
|
||||
return saveWechatLoginResult(result);
|
||||
}
|
||||
|
||||
/** 已登录用户绑定微信:小程序 code;H5 走公众号 OAuth(带 JWT 时服务端会 attach) */
|
||||
export async function bindWechatForUser(
|
||||
prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null,
|
||||
): Promise<WechatBindResult> {
|
||||
if (process.env.TARO_ENV === 'h5') {
|
||||
const result = await authorizeWechatForPay();
|
||||
if (!result) {
|
||||
return { ok: false, redirecting: true };
|
||||
}
|
||||
if (result.needBindPhone && result.wxSessionKey) {
|
||||
return { ok: false, needBindPhone: true, wxSessionKey: result.wxSessionKey };
|
||||
}
|
||||
if (result.accessToken) {
|
||||
saveAuth({
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
});
|
||||
}
|
||||
const profile = await fetchUserProfile();
|
||||
return { ok: true, profile };
|
||||
}
|
||||
|
||||
const res = await Taro.login();
|
||||
if (!res.code) {
|
||||
throw new Error(res.errMsg || '微信授权失败');
|
||||
}
|
||||
const { request } = await import('./api');
|
||||
const data = await request<WechatLoginResult>('/auth/wechat/bind', {
|
||||
method: 'POST',
|
||||
data: { code: res.code, platform: 'mini' },
|
||||
});
|
||||
if (data.needBindPhone && data.wxSessionKey) {
|
||||
return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
|
||||
}
|
||||
await syncMiniWechatProfile(prefetchedWxProfile);
|
||||
const profile = await fetchUserProfile();
|
||||
return { ok: true, profile };
|
||||
}
|
||||
|
||||
export type { WechatBindResult };
|
||||
|
||||
Reference in New Issue
Block a user