feat: add avatar payments and finance management
This commit is contained in:
@@ -61,7 +61,7 @@ H5 引入 uniapp web-view bridge 后调用:
|
||||
壳通过 `web-view.evalJS` 调用 H5 全局函数 `window.__uniBridgeHandle__(message)`:
|
||||
| type | payload | 含义 |
|
||||
|------|---------|------|
|
||||
| `context` | `platform, version` | 注入运行环境信息 |
|
||||
| `context` | `surface, version` | 注入运行环境;`surface` 为 `app` / `mp-weixin` / `h5` |
|
||||
| `tokenRefresh` | `token` | 登录刷新后下发新 token |
|
||||
| `userUpdate` | `user` | 会会资料变更 |
|
||||
| `paymentResult` | `orderId,status` | 原生支付结束通知;`status` 为 `success/cancelled/failed` |
|
||||
@@ -70,6 +70,8 @@ H5 引入 uniapp web-view bridge 后调用:
|
||||
|
||||
壳收到 `payment` 后应调用会会 App 已有的微信/支付宝支付能力(或 `uni.requestPayment`),把 `payMessage/paymentParams` 原样交给对应渠道。原生 SDK 返回后再发送 `paymentResult`;H5 不以原生返回作为到账依据,只会轮询本地订单,最终由会会服务端支付回调确认并增加积分。
|
||||
|
||||
微信小程序虚拟支付本期只交付后端能力(登录态交换、签名下单参数、服务端查单/退款和回调验收)。小程序原生充值页接入 `requestVirtualPayment` 后,应把后端返回的 `signData/paySig/signature/mode/env/offerId` 原样传入微信 API;不要在 web-view 中发起虚拟支付。
|
||||
|
||||
---
|
||||
|
||||
## 3. 项目结构(uni CLI / src 布局,已验证可编译)
|
||||
@@ -84,13 +86,13 @@ uniapp-avatar/
|
||||
├── manifest.json # 应用配置(名称/AppID/模块)
|
||||
├── pages.json # 页面路由
|
||||
├── uni.scss # 全局样式变量
|
||||
├── App.vue # 启动即做会会登录(onLaunch → userStore.init)
|
||||
├── App.vue # 启动时恢复会会登录态(onLaunch → userStore.init)
|
||||
├── main.js # createSSRApp + pinia
|
||||
├── pages/index/index.vue # web-view 容器(内嵌 digital-avatar-app H5)
|
||||
├── store/user.js # 会会会话(token/资料,本地缓存)
|
||||
├── store/user.js # 会会会话(宿主调用 applySession 注入并缓存)
|
||||
└── utils/
|
||||
├── bridge.js # H5 URL 构造 + 原生→H5 推送
|
||||
└── huihui.js # 会会登录(MOCK,留真实接入位)
|
||||
└── payment.js # App 微信/支付宝原生支付适配
|
||||
```
|
||||
> 构建产物:`npm run build:h5` → `dist/build/h5/`(含 index.html + assets)。
|
||||
|
||||
@@ -114,10 +116,9 @@ npm run build:h5 # 生产构建 → dist/build/h5/
|
||||
> 若 npm 依赖版本与本地 HBuilderX 不一致,执行 `npx @dcloudio/uvm` 对齐。
|
||||
|
||||
### 会会登录接入
|
||||
- 当前 `utils/huihui.js` 为 **MOCK**(`MOCK_AUTH = true`),便于联调。
|
||||
- 生产接入:把 `MOCK_AUTH` 改为 `false`,在 `loginHuihui()` 接入会会开放平台授权,
|
||||
换取 `access_token`、`userId`;`getUserInfo()` 请求会会 `usercenter` 真实资料接口
|
||||
(接口基址见 `docs/production-interface-inventory.md`)。
|
||||
- 壳只恢复会会宿主已经持有的登录态,不内置演示账号,也不会伪造会会 token。
|
||||
- 会会主 App 完成登录或刷新后调用 `userStore.applySession({ token, userId, nickname, avatarUrl })`;数字分身 H5 会把一次性会会 token 换成本系统会话并立即从地址中清除。
|
||||
- 独立打开且没有宿主会话时,H5 会进入已有的短信登录流程。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
+7937
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "uniapp-avatar",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:h5": "uni -p h5",
|
||||
"build:h5": "uni build -p h5",
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"dev:app": "uni -p app",
|
||||
"build:app": "uni build -p app"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4010520240507001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4010520240507001",
|
||||
"@dcloudio/uni-components": "3.0.0-4010520240507001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4010520240507001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4010520240507001",
|
||||
"pinia": "^2.0.36",
|
||||
"vue": "^3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4010520240507001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4010520240507001",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
import { onLaunch } from '@dcloudio/uni-app'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
onLaunch(() => useUserStore().init())
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page { background:#f5f6f8; }
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(createPinia())
|
||||
return { app }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "会会数字分身",
|
||||
"appid": "",
|
||||
"description": "会会数字分身 uni-app 混合壳",
|
||||
"versionName": "1.1.0",
|
||||
"versionCode": "110",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"compilerVersion": 3,
|
||||
"modules": { "Payment": {} },
|
||||
"distribute": {
|
||||
"android": { "permissions": ["<uses-permission android:name=\"android.permission.INTERNET\"/>"] },
|
||||
"ios": {},
|
||||
"sdkConfigs": {}
|
||||
}
|
||||
},
|
||||
"mp-weixin": { "appid": "", "setting": { "urlCheck": true }, "usingComponents": true },
|
||||
"h5": { "publicPath": "./", "router": { "base": "./" } },
|
||||
"vueVersion": "3"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"pages": [{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "会会数字分身",
|
||||
"navigationBarBackgroundColor": "#0F2B4C",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
}],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "会会数字分身",
|
||||
"navigationBarBackgroundColor": "#0F2B4C",
|
||||
"backgroundColor": "#F5F6F8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<view class="shell">
|
||||
<web-view ref="webview" :src="h5Url" @message="onH5Message" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { buildH5Url, parseH5Message, postToH5 } from '@/utils/bridge'
|
||||
import { requestAppPayment } from '@/utils/payment'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const webview = ref(null)
|
||||
const H5_BASE_URL = import.meta.env.VITE_AVATAR_H5_URL || 'https://digital.99hui.com/'
|
||||
const h5Url = computed(() => buildH5Url(H5_BASE_URL, userStore.$state))
|
||||
|
||||
function runtimeSurface() {
|
||||
let value = 'h5'
|
||||
// #ifdef APP-PLUS
|
||||
value = 'app'
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
value = 'mp-weixin'
|
||||
// #endif
|
||||
return value
|
||||
}
|
||||
|
||||
async function handlePayment(payment) {
|
||||
try {
|
||||
await requestAppPayment(payment)
|
||||
postToH5(webview.value, { type: 'paymentResult', orderId: payment.orderId, status: 'success' })
|
||||
} catch (error) {
|
||||
const message = error?.message || '支付未完成'
|
||||
const status = /cancel/i.test(message) || /取消/.test(message) ? 'cancelled' : 'failed'
|
||||
postToH5(webview.value, { type: 'paymentResult', orderId: payment.orderId, status, message })
|
||||
}
|
||||
}
|
||||
|
||||
function onH5Message(event) {
|
||||
const message = parseH5Message(event)
|
||||
if (!message?.type) return
|
||||
if (message.type === 'ready') {
|
||||
postToH5(webview.value, { type: 'context', surface: runtimeSurface(), version: '1.1.0' })
|
||||
} else if (message.type === 'payment') {
|
||||
void handlePayment(message.payment)
|
||||
} else if (message.type === 'setTitle' && message.title) {
|
||||
uni.setNavigationBarTitle({ title: message.title })
|
||||
} else if (message.type === 'back') {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>.shell{width:100%;height:100vh}</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({ token: '', userId: '', nickname: '', avatarUrl: '', initialized: false }),
|
||||
getters: { isLogin: state => Boolean(state.token) },
|
||||
actions: {
|
||||
init() {
|
||||
this.$patch({
|
||||
token: uni.getStorageSync('hh_token') || '',
|
||||
userId: uni.getStorageSync('hh_userId') || '',
|
||||
nickname: uni.getStorageSync('hh_nickname') || '',
|
||||
avatarUrl: uni.getStorageSync('hh_avatarUrl') || '',
|
||||
initialized: true
|
||||
})
|
||||
},
|
||||
applySession(session) {
|
||||
this.$patch(session)
|
||||
uni.setStorageSync('hh_token', this.token)
|
||||
uni.setStorageSync('hh_userId', this.userId)
|
||||
uni.setStorageSync('hh_nickname', this.nickname)
|
||||
uni.setStorageSync('hh_avatarUrl', this.avatarUrl)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
$brand-navy: #0f2b4c;
|
||||
$brand-orange: #f97316;
|
||||
$bg-page: #f5f6f8;
|
||||
@@ -0,0 +1,33 @@
|
||||
export function buildH5Url(base, session) {
|
||||
const url = new URL(base)
|
||||
if (session.token) url.searchParams.set('token', session.token)
|
||||
if (session.userId) url.searchParams.set('userId', session.userId)
|
||||
if (session.nickname) url.searchParams.set('nickname', session.nickname)
|
||||
if (session.avatarUrl) url.searchParams.set('avatar', session.avatarUrl)
|
||||
url.searchParams.set('ts', String(Date.now()))
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
export function postToH5(webviewRef, message) {
|
||||
if (!webviewRef) return false
|
||||
const js = `window.__uniBridgeHandle__&&window.__uniBridgeHandle__(${JSON.stringify(message)})`
|
||||
try {
|
||||
if (typeof webviewRef.evalJS === 'function') {
|
||||
webviewRef.evalJS(js)
|
||||
return true
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
const child = plus.webview.currentWebview().children()[0]
|
||||
if (child) { child.evalJS(js); return true }
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.error('[bridge] postToH5 failed', error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function parseH5Message(event) {
|
||||
const items = event?.detail?.data
|
||||
if (Array.isArray(items) && items.length) return items[items.length - 1]
|
||||
return event?.detail || null
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
function parseOrderInfo(payment) {
|
||||
if (payment.paymentParams && typeof payment.paymentParams === 'object') return payment.paymentParams
|
||||
if (typeof payment.payMessage !== 'string') return payment.payMessage
|
||||
try { return JSON.parse(payment.payMessage) } catch { return payment.payMessage }
|
||||
}
|
||||
|
||||
export function requestAppPayment(payment) {
|
||||
if (!payment || payment.payWay !== 'APP') {
|
||||
return Promise.reject(new Error('当前订单不是 App 支付订单'))
|
||||
}
|
||||
const provider = payment.paymentMethod === 'alipay' ? 'alipay' : 'wxpay'
|
||||
const orderInfo = parseOrderInfo(payment)
|
||||
if (!orderInfo) return Promise.reject(new Error('支付参数为空'))
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.requestPayment({
|
||||
provider,
|
||||
orderInfo,
|
||||
success: resolve,
|
||||
fail: error => reject(new Error(error?.errMsg || '支付未完成'))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
|
||||
const uniPlugin = typeof uni === 'function' ? uni : uni.default
|
||||
export default defineConfig({ plugins: [uniPlugin()] })
|
||||
Reference in New Issue
Block a user