feat(binding): verify 4G relations before Banban unbind

This commit is contained in:
stu2not
2026-09-23 17:47:42 +08:00
parent e5ba92ba13
commit 515567f6d4
26 changed files with 2185 additions and 29 deletions
+5
View File
@@ -26,6 +26,7 @@ import {
validateChildNameInput,
} from '@/services/child'
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
import { trafficPageUrl } from '@/services/traffic-binding'
import './index.scss'
@@ -678,6 +679,10 @@ export default function Bind() {
<Button className='bind-result-button primary' onClick={() => Taro.reLaunch({ url: bindResultDestination })}>
{bindResultDestination.includes('/sleep/') ? '去关联宝贝' : '返回首页'}
</Button>
{!isAddingCard && <Button className='bind-result-button primary' onClick={() => Taro.navigateTo({ url: trafficPageUrl(boundDeviceId) })}>
关联流量服务
</Button>}
{!isAddingCard && <Text className='bind-result-description'>设备已绑定。流量查询和充值需另外关联,可稍后在管理页完成。</Text>}
</>
) : (
<>
+37 -11
View File
@@ -10,6 +10,7 @@ import Taro, { useDidHide, useDidShow, useShareAppMessage } from '@tarojs/taro'
import { authorizeParentPhone, clearToken, getParent, getToken, Parent } from '@/services/auth'
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
import { getCurrentUserId } from '@/services/session'
import { TrafficOverview, getTrafficOverview, trafficLinked, openTrafficPage, openTrafficPortal, flushTrafficReceipt } from '@/services/traffic-binding'
import {
BindingListItem,
BindingCardItem,
@@ -19,7 +20,6 @@ import {
loadCurrentChildBindingContext,
setSelectedBindingDeviceId,
updateBindingCardName,
unbindDevice,
} from '@/services/binding'
import {
Child,
@@ -77,6 +77,20 @@ export default function Sleep() {
const [children, setChildren] = useState<Child[]>([])
const [bindings, setBindings] = useState<BindingListItem[]>([])
const [binding, setBinding] = useState<BindingListItem | null>(null)
const [trafficOverview, setTrafficOverview] = useState<TrafficOverview | null>(null)
const [trafficBusy, setTrafficBusy] = useState(false)
const trafficLock = useRef(false)
useEffect(() => {
let cancelled = false
setTrafficOverview(null)
if (binding?.device_id) {
void flushTrafficReceipt().catch(() => null)
.then(() => getTrafficOverview(binding.device_id))
.then((value) => { if (!cancelled) setTrafficOverview(value) })
.catch(() => { /* Core device management remains available. */ })
}
return () => { cancelled = true }
}, [binding])
const [currentChild, setCurrentChild] = useState<Child | null>(null)
const [firmwareStatus, setFirmwareStatus] = useState<DeviceFirmwareStatus | null>(null)
const [showFirmwareProgress, setShowFirmwareProgress] = useState(false)
@@ -651,12 +665,7 @@ export default function Sleep() {
if (!result.confirm) return
try {
await unbindDevice(targetBinding.device_id)
if (binding?.device_id === targetBinding.device_id) {
clearSelectedBindingDeviceId()
}
Taro.showToast({ title: '已删除设备', icon: 'success' })
await loadData()
await openTrafficPage(targetBinding.device_id, 'unbind')
} catch (error: any) {
Taro.showToast({
title: error?.message || '删除设备失败,请重试',
@@ -834,10 +843,7 @@ export default function Sleep() {
if (!result.confirm) return
try {
await unbindDevice(binding.device_id)
clearSelectedBindingDeviceId()
Taro.showToast({ title: '已解绑', icon: 'success' })
await loadData()
await openTrafficPage(binding.device_id, 'unbind')
} catch (error: any) {
Taro.showToast({
title: error?.message || '解绑失败,请重试',
@@ -956,6 +962,18 @@ export default function Sleep() {
return
}
if (['关联流量服务', '继续确认流量服务', '继续解绑', '流量查询', '套餐充值'].includes(item.name)) {
if (!binding || trafficLock.current) return
trafficLock.current = true
setTrafficBusy(true)
const operation = item.name === '流量查询' || item.name === '套餐充值'
? openTrafficPortal(binding.device_id, item.name === '套餐充值' ? 'recharge' : 'device')
: openTrafficPage(binding.device_id, item.name === '继续解绑' ? 'unbind' : 'bind')
void operation.catch((error: any) => Taro.showToast({ title: error.message || error.errMsg || '操作未完成,请重试', icon: 'none' }))
.finally(() => { trafficLock.current = false; setTrafficBusy(false) })
return
}
if (item.name === '我的卡片') {
setShowCardsModal(true)
return
@@ -1129,6 +1147,14 @@ export default function Sleep() {
value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定',
arrow: true,
},
...(binding ? (trafficLinked(trafficOverview) && trafficOverview?.device_id === binding.device_id
? [
{ icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green', name: '流量查询', value: '', arrow: true, disabled: !isFamilyOwner || trafficBusy },
{ icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green', name: '套餐充值', value: '', arrow: true, disabled: !isFamilyOwner || trafficBusy },
]
: [{ icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green',
name: trafficOverview?.unbind ? '继续解绑' : trafficOverview?.bind ? '继续确认流量服务' : '关联流量服务',
value: isFamilyOwner ? '不影响伴伴使用' : '仅设备归属账号可操作', arrow: true, disabled: !isFamilyOwner || trafficBusy }]) : []),
{
icon: require('../../assets/ui/menu-cards.png'),
iconBgClass: 'green',
@@ -0,0 +1 @@
export default definePageConfig({ navigationBarTitleText: '流量服务关联' })
@@ -0,0 +1,7 @@
.traffic-binding-page {
min-height: 100vh; box-sizing: border-box; padding: 72rpx 40rpx;
.title { display: block; font-size: 38rpx; font-weight: bold; margin-bottom: 30rpx; }
.description { display: block; font-size: 28rpx; line-height: 1.7; margin-bottom: 30rpx; }
.error { display: block; color: #a52a2a; margin-bottom: 24rpx; }
button { margin-top: 24rpx; }
}
@@ -0,0 +1,69 @@
import { View, Text, Button } from '@tarojs/components'
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
import { useRef, useState } from 'react'
import { getToken } from '@/services/auth'
import { clearSelectedBindingDeviceId } from '@/services/binding'
import { getPageStyle } from '@/utils/page-style'
import { TrafficOperation, getTrafficOperation, resumeTrafficOperation,
flushTrafficReceipt, finishTrafficUnbind, jumpTraffic } from '@/services/traffic-binding'
import './index.scss'
export default function TrafficBindingPage() {
const { params } = useRouter()
const deviceId = String(params.device_id || '')
const action = params.action === 'unbind' ? 'unbind' : 'bind'
const [operation, setOperation] = useState<TrafficOperation | null>(null)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const lock = useRef(false)
const recover = async () => {
if (lock.current) return
if (!getToken()) { Taro.navigateTo({ url: '/pages/login/index' }); return }
lock.current = true
setBusy(true)
try {
const current = await resumeTrafficOperation(deviceId, action)
setOperation(current)
if (action === 'unbind' && current.status === 'completed') clearSelectedBindingDeviceId()
setError('')
} catch (e: any) { setError(e.message || '暂时无法确认,请重试') }
finally { lock.current = false; setBusy(false) }
}
useDidShow(() => { void recover() })
const proceed = async () => {
if (!operation) { await recover(); return }
if (lock.current) return
lock.current = true
setBusy(true)
setError('')
try {
await flushTrafficReceipt()
let current = await getTrafficOperation(operation.operation_id)
if (current.status !== 'completed') {
const result = await jumpTraffic(current)
if (!result) return
current = result
if (action === 'unbind') current = await finishTrafficUnbind(current)
}
setOperation(current)
if (action === 'unbind' && current.status === 'completed') clearSelectedBindingDeviceId()
} catch (e: any) { setError(e.message || e.errMsg || '操作未完成,请继续重试') }
finally { lock.current = false; setBusy(false) }
}
const completed = operation?.status === 'completed'
const finalUnbind = action === 'unbind' && operation?.traffic_status === 'server_confirmed'
return <View className='traffic-binding-page' style={getPageStyle(true)}>
<Text className='title'>{action === 'bind' ? completed ? '流量服务已关联' : '设备已绑定,流量服务待关联' : completed ? '设备已解除绑定' : '解除设备与流量服务绑定'}</Text>
<Text className='description'>{action === 'bind'
? completed ? '可以查询流量和购买套餐了。' : '伴伴设备可以正常使用。继续前往流量管理小程序登录并关联,也可以稍后完成。'
: completed ? '设备已解绑,流量侧已确认没有本次有效绑定。' : finalUnbind ? '流量后端已确认可以解绑,点击完成伴伴解绑。' : '先确认流量侧原绑定已解除,再解除伴伴绑定;套餐和充值记录保留。继续处理会停止本次流量关联,暂不支持取消恢复。'}</Text>
{operation?.last_error === 'HANDOFF_RELATION_UNKNOWN' && <Text className='error'>流量后端无法确认原绑定记录,伴伴绑定仍保留,请联系服务人员核查。</Text>}
{operation?.last_error === 'TRAFFIC_UNBIND_REQUIRED' && <Text className='description'>原流量绑定仍有效,请继续前往流量管理小程序解绑。</Text>}
{operation?.status === 'partial_failed' && <Text className='description'>上次流量操作未完成,可以继续重试。</Text>}
{error && <Text className='error'>{error}</Text>}
{!completed && <Button disabled={busy} onClick={proceed}>{busy ? '处理中…' : finalUnbind ? '完成伴伴解绑' : '继续处理'}</Button>}
<Button disabled={busy} onClick={() => Taro.switchTab({ url: '/pages/sleep/index' })}>{completed ? '返回管理' : '稍后处理'}</Button>
</View>
}