微信支付

This commit is contained in:
dqz 2025-03-17 08:30:57 +08:00
parent db4b454898
commit a42638c17b
11 changed files with 230 additions and 96 deletions

View File

@ -1,4 +1,4 @@
# 全局 ts 类型检查(此操作会增加 git commit 时长)
npx vue-tsc
# npx vue-tsc
# 执行 lint-staged 中配置的任务
npx lint-staged
# npx lint-staged

View File

@ -2,8 +2,11 @@
import Layout from "@/layout/index.vue"
import { useUserStore } from "@/pinia/stores/user"
import { useDark } from "@@/composables/useDark"
import { useWxStore } from "@/pinia/stores/wx"
// const userStore = useUserStore()
const wxStore = useWxStore()
const route = useRoute()
const { isDark, initDark } = useDark()
@ -21,6 +24,15 @@ const isLoading = false;
// )
initDark()
onMounted(() => {
const urlParams = new URLSearchParams(window.location.search);
console.log('urlParams', urlParams);
const code = urlParams.get('code') || undefined;
const state = urlParams.get('state') || undefined;
if (code || state) {
wxStore.handleWxCallback({ code, state })
}
})
</script>
<template>

View File

@ -1,5 +1,7 @@
import { request } from "@/http/axios"
import { ShopGoodsResponseData } from './type'
import { ShopGoodsResponseData, SubmitOrderRequestData, SubmitOrderResponseData } from './type'
import { GetOpenIdRequestParams } from './type'
/** 获取当前登录用户详情 */
export function getShopGoodsApi() {
@ -8,3 +10,21 @@ export function getShopGoodsApi() {
method: "get"
})
}
/** 提交订单接口 */
export function submitOrderApi(data: SubmitOrderRequestData) {
return request<SubmitOrderResponseData>({
url: "order/submit",
method: "post",
data
})
}
/** 获取微信openid */
export function getOpenIdApi(params: GetOpenIdRequestParams) {
return request<string>({
url: "payment/getOpenId",
method: "get",
params // 使用params传递code参数对应Java的@RequestParam
})
}

View File

@ -15,7 +15,35 @@ export type category = {
sort: number
}
export interface SubmitOrderRequestData {
openid: string
goodsList: Array<{
goodsId: number
quantity: number
}>
}
export type SubmitOrderResponseData = ApiResponseMsgData<{
orderId: number
totalAmount: number
paymentInfo: WxJsApiPreCreateResponse
}>
export type ShopGoodsResponseData = ApiResponseMsgData<{
goodsList: Goods[],
categoryList: category[]
}>
export interface WxJsApiPreCreateResponse {
appId: string
timeStamp: string
nonceStr: string
/** @JsonProperty("package") */
packageValue: string
signType: string
paySign: string
}
export interface GetOpenIdRequestParams {
code: string
}

18
src/common/utils/wx.ts Normal file
View File

@ -0,0 +1,18 @@
function openInWeChat(url: string) {
const weChatUrl = `weixin://dl/business/?t=${encodeURIComponent(url)}`;
window.location.href = weChatUrl;
// 检测跳转是否成功
setTimeout(() => {
if (document.hidden) return;
alert("未检测到微信,请手动打开微信并访问链接。");
}, 2000);
}
function checkInWeChat(targetUrl: string) {
const ua = navigator.userAgent.toLowerCase();
if (!ua.includes('micromessenger')) {
openInWeChat(targetUrl); // 调用上述跳转函数
} else {
console.log("已在微信内,无需跳转");
}
}

View File

@ -1,25 +1,57 @@
<script setup lang="ts">
import { useCartStore } from "@/pinia/stores/cart"
import { useWxStore } from "@/pinia/stores/wx"
import { storeToRefs } from "pinia"
import { showConfirmDialog } from "vant"
import { ref } from "vue"
import { submitOrderApi } from "@/common/apis/shop"
import type { SubmitOrderRequestData, WxJsApiPreCreateResponse } from "@/common/apis/shop/type"
import { useRouter } from 'vue-router'
const router = useRouter()
const cartStore = useCartStore()
const { cartItems, totalPrice } = storeToRefs(cartStore)
//
const paymentMethods = [
{ value: 1, label: "微信支付", icon: "wechat" },
{ value: 2, label: "支付宝", icon: "alipay" },
{ value: 3, label: "银行卡支付", icon: "credit-pay" }
]
const wxStore = useWxStore()
const { openid } = storeToRefs(wxStore)
const selectedPayment = ref<number>()
const contact = ref("")
const remark = ref("")
const submitting = ref(false)
//
function callWxJsApi(paymentInfo: WxJsApiPreCreateResponse) {
return new Promise((resolve, reject) => {
function onBridgeReady() {
(window as any).WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
appId: paymentInfo.appId,
timeStamp: paymentInfo.timeStamp,
nonceStr: paymentInfo.nonceStr,
package: paymentInfo.packageValue.startsWith('prepay_id=')
? paymentInfo.packageValue
: `prepay_id=${paymentInfo.packageValue}`,
signType: paymentInfo.signType,
paySign: paymentInfo.paySign
},
(res: { err_msg: string }) => {
if (res.err_msg === "get_brand_wcpay_request:ok") {
resolve(true);
} else {
reject(new Error('支付未完成'));
}
}
);
}
if (typeof (window as any).WeixinJSBridge === 'undefined') {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else {
onBridgeReady();
}
});
}
async function handleSubmit() {
if (!cartItems.value.length) {
return showConfirmDialog({
@ -28,31 +60,53 @@ async function handleSubmit() {
})
}
if (!selectedPayment.value) {
if (!openid.value) {
return showConfirmDialog({
title: "提示",
message: "请选择支付方式"
title: "登录提示",
message: "请从微信中打开"
})
}
submitting.value = true
try {
// TODO: API
console.log("提交订单", {
items: cartItems.value,
total: totalPrice.value,
paymentMethod: selectedPayment.value,
contact: contact.value,
remark: remark.value
})
//
const requestData: SubmitOrderRequestData = {
openid: openid.value, // openid
goodsList: cartItems.value.map(item => ({
goodsId: item.product.id,
quantity: item.quantity
}))
}
//
const { data } = await submitOrderApi(requestData)
await showConfirmDialog({
title: "提交成功",
message: "订单已创建,正在跳转支付..."
message: `订单号:${data.orderId},正在跳转支付...`
})
//
if (data.paymentInfo) {
await callWxJsApi(data.paymentInfo);
//
router.push('/order-success');
} else {
throw new Error('无法获取支付信息');
}
//
cartStore.clearCart()
// paymentInfo
// JSAPI
} catch (error) {
if(error !== 'user_cancel') {
showConfirmDialog({
title: "支付失败",
message: error instanceof Error ? error.message : "支付流程中断"
});
}
} finally {
submitting.value = false
}
@ -61,31 +115,14 @@ async function handleSubmit() {
<template>
<div class="checkout-container">
<!-- 新增固定导航栏 -->
<van-nav-bar
title="结算页面"
left-text="返回"
left-arrow
fixed
@click-left="() => $router.go(-1)"
/>
<van-nav-bar title="结算页面" left-text="返回" left-arrow fixed @click-left="() => $router.go(-1)" />
<!-- 原有内容容器 -->
<div class="content-wrapper">
<!-- 原有商品列表等代码保持不动 -->
<van-cell-group class="product-list">
<van-cell
v-for="item in cartItems"
:key="item.product.id"
class="product-item"
>
<van-cell v-for="item in cartItems" :key="item.product.id" class="product-item">
<template #icon>
<van-image
:src="item.product.image"
width="60"
height="60"
class="product-image"
/>
<van-image :src="item.product.image" width="60" height="60" class="product-image" />
</template>
<div class="product-info">
@ -106,45 +143,11 @@ async function handleSubmit() {
<!-- 联系方式与备注 -->
<van-cell-group class="contact-form">
<van-field
v-model="contact"
label="联系方式"
placeholder="请输入手机号"
:rules="[
{ required: true, message: '请填写联系方式' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
]"
/>
<van-field
v-model="remark"
label="备注"
type="textarea"
placeholder="选填,可备注特殊需求"
rows="2"
autosize
/>
</van-cell-group>
<!-- 支付方式选择 -->
<van-cell-group title="支付方式" class="payment-methods">
<van-radio-group v-model="selectedPayment">
<van-cell
v-for="method in paymentMethods"
:key="method.value"
clickable
@click="selectedPayment = method.value"
>
<template #icon>
<van-icon :name="method.icon" class="method-icon" />
</template>
<template #title>
<span class="method-label">{{ method.label }}</span>
</template>
<template #right-icon>
<van-radio :name="method.value" />
</template>
</van-cell>
</van-radio-group>
<van-field v-model="contact" label="联系方式" placeholder="请输入手机号" :rules="[
{ required: true, message: '请填写联系方式' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
]" />
<van-field v-model="remark" label="备注" type="textarea" placeholder="选填,可备注特殊需求" rows="2" autosize />
</van-cell-group>
<!-- 提交订单栏 -->
@ -152,13 +155,7 @@ async function handleSubmit() {
<div class="total-price">
合计¥{{ totalPrice.toFixed(2) }}
</div>
<van-button
type="primary"
size="large"
:loading="submitting"
loading-text="提交中..."
@click="handleSubmit"
>
<van-button type="primary" size="large" :loading="submitting" loading-text="提交中..." @click="handleSubmit">
提交订单
</van-button>
</div>
@ -177,7 +174,8 @@ async function handleSubmit() {
}
.content-wrapper {
padding-top: 46px; /* 导航栏高度 */
padding-top: 46px;
/* 导航栏高度 */
}
.checkout-container {

59
src/pinia/stores/wx.ts Normal file
View File

@ -0,0 +1,59 @@
import { pinia } from "@/pinia"
import { getOpenIdApi } from "@/common/apis/shop"
export const useWxStore = defineStore("wx", () => {
// 微信授权 code
const code = ref<string>("")
// 防止 CSRF 攻击的 state 参数
const state = ref<string>("")
// 用户 openid
const openid = ref<string>("")
// 模拟微信授权获取 code需对接实际微信接口
const getWxAuth = async () => {
// 这里应替换为实际微信授权逻辑
return new Promise<void>((resolve) => {
setTimeout(() => {
code.value = "模拟的微信code"
state.value = Date.now().toString()
resolve()
}, 1000)
})
}
// 设置 openid
const setOpenid = (id: string) => {
openid.value = id
}
const handleWxCallback = async (params: { code?: string; state?: string }) => {
console.log('handleWxCallback:', params)
if (params.code) {
code.value = params.code
state.value = params.state || state.value
try {
// 调用获取 openid 的接口
const res = await getOpenIdApi({ code: params.code })
console.log('获取 openid 成功:', res)
if (res) {
openid.value = res
}
} catch (err) {
console.error('获取 openid 失败:', err)
}
}
}
(window as any).testWxSetOpenid = setOpenid
return { code, state, openid, getWxAuth, setOpenid, handleWxCallback }
})
/**
* @description setup 使 store
*/
export function useWxStoreOutside() {
return useWxStore(pinia)
}

View File

@ -4,5 +4,5 @@ import { installPermissionDirective } from "./permission-directive"
export function installPlugins(app: App) {
installPermissionDirective(app)
// installConsole()
installConsole()
}

View File

@ -6,6 +6,7 @@ import { useTitle } from "@@/composables/useTitle"
import { getToken } from "@@/utils/cache/cookies"
import NProgress from "nprogress"
NProgress.configure({ showSpinner: false })
const { setTitle } = useTitle()

View File

@ -21,8 +21,6 @@ declare module 'vue' {
VanImage: typeof import('vant/es')['Image']
VanLoading: typeof import('vant/es')['Loading']
VanNavBar: typeof import('vant/es')['NavBar']
VanRadio: typeof import('vant/es')['Radio']
VanRadioGroup: typeof import('vant/es')['RadioGroup']
VanSidebar: typeof import('vant/es')['Sidebar']
VanSidebarItem: typeof import('vant/es')['SidebarItem']
VanTabbar: typeof import('vant/es')['Tabbar']

View File

@ -82,7 +82,7 @@ export default defineConfig(({ mode }) => {
? undefined
: {
// 打包构建时移除 console.log
pure: ["console.log"],
// pure: ["console.log"],
// 打包构建时移除 debugger
drop: ["debugger"],
// 打包构建时移除所有注释