feat(glt): 完善水票总数获取逻辑并优化用户体验

- 新增 normalizeTotal 函数处理多种数据格式的总数解析
- 支持通过 userId 参数查询指定用户的水票总数
- 添加多服务器地址尝试机制提高接口可用性
- 优化首页和用户中心页面的水票总数加载逻辑
- 修复水票页面滚动区域高度计算问题
- 移除自动跳转登录页面的定时器逻辑
- 个人中心页面支持下拉刷新统计数据
- 统一请求参数传递方式简化代码结构
This commit is contained in:
2026-02-05 01:35:11 +08:00
parent 526c821a67
commit 5e90c48b8b
8 changed files with 112 additions and 40 deletions

View File

@@ -1,6 +1,30 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api'; import type { ApiResult, PageResult } from '@/api';
import type { GltUserTicket, GltUserTicketParam } from './model'; import type { GltUserTicket, GltUserTicketParam } from './model';
import { SERVER_API_URL } from '@/utils/server'
function normalizeTotal(input: unknown): number {
if (typeof input === 'number' && Number.isFinite(input)) return input;
if (typeof input === 'string') {
const n = Number(input);
if (Number.isFinite(n)) return n;
}
if (input && typeof input === 'object') {
const obj: any = input;
// Common shapes from different backends.
for (const key of ['total', 'count', 'value', 'num', 'ticketTotal', 'totalQty']) {
const v = obj?.[key];
const n = normalizeTotal(v);
if (n) return n;
}
// Sometimes nested: { data: { total: ... } } / { data: 12 }
if ('data' in obj) {
const n = normalizeTotal(obj.data);
if (n) return n;
}
}
return 0;
}
/** /**
* 分页查询我的水票 * 分页查询我的水票
@@ -8,9 +32,7 @@ import type { GltUserTicket, GltUserTicketParam } from './model';
export async function pageGltUserTicket(params: GltUserTicketParam) { export async function pageGltUserTicket(params: GltUserTicketParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicket>>>( const res = await request.get<ApiResult<PageResult<GltUserTicket>>>(
'/glt/glt-user-ticket/page', '/glt/glt-user-ticket/page',
{ params
params
}
); );
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
return res.data; return res.data;
@@ -24,9 +46,7 @@ export async function pageGltUserTicket(params: GltUserTicketParam) {
export async function listGltUserTicket(params?: GltUserTicketParam) { export async function listGltUserTicket(params?: GltUserTicketParam) {
const res = await request.get<ApiResult<GltUserTicket[]>>( const res = await request.get<ApiResult<GltUserTicket[]>>(
'/glt/glt-user-ticket', '/glt/glt-user-ticket',
{ params
params
}
); );
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
return res.data; return res.data;
@@ -107,15 +127,46 @@ export async function getGltUserTicket(id: number) {
/** /**
* 获取我的水票总数 * 获取我的水票总数
*/ */
export async function getMyGltUserTicketTotal() { export async function getMyGltUserTicketTotal(userId?: number) {
const res = await request.get<ApiResult<number | { total?: number }>>( const params = userId ? { userId } : undefined
'/glt/glt-user-ticket/my-total'
); const extract = (res: any) => {
if (res.code === 0) { // Some backends may return a raw number instead of ApiResult.
const data: any = res.data; if (typeof res === 'number' || typeof res === 'string') return normalizeTotal(res)
if (typeof data === 'number') return data; if (res && typeof res === 'object' && 'code' in res) {
if (data && typeof data.total === 'number') return data.total; const apiRes = res as ApiResult<unknown>
return 0; if (apiRes.code === 0) return normalizeTotal(apiRes.data)
throw new Error(apiRes.message)
}
return normalizeTotal(res)
} }
return Promise.reject(new Error(res.message));
// Try both the configured BaseUrl host and the auth-server host.
// If the first one returns 0, keep trying; some tenants deploy GLT on a different host.
const urls = [
'/glt/glt-user-ticket/my-total',
`${SERVER_API_URL}/glt/glt-user-ticket/my-total`,
]
let lastError: unknown
let firstTotal: number | undefined
for (const url of urls) {
try {
const res = await request.get<any>(url, params)
if (process.env.NODE_ENV === 'development') {
console.log('[getMyGltUserTicketTotal] response:', { url, res })
}
const total = extract(res)
if (firstTotal === undefined) firstTotal = total
if (total) return total
} catch (e) {
if (process.env.NODE_ENV === 'development') {
console.warn('[getMyGltUserTicketTotal] failed:', { url, error: e })
}
lastError = e
}
}
if (firstTotal !== undefined) return firstTotal
return Promise.reject(lastError instanceof Error ? lastError : new Error('获取水票总数失败'))
} }

View File

@@ -8,9 +8,7 @@ import type { GltUserTicketLog, GltUserTicketLogParam } from './model';
export async function pageGltUserTicketLog(params: GltUserTicketLogParam) { export async function pageGltUserTicketLog(params: GltUserTicketLogParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicketLog>>>( const res = await request.get<ApiResult<PageResult<GltUserTicketLog>>>(
'/glt/glt-user-ticket-log/page', '/glt/glt-user-ticket-log/page',
{ params
params
}
); );
if (res.code === 0) { if (res.code === 0) {
return res.data; return res.data;
@@ -24,9 +22,7 @@ export async function pageGltUserTicketLog(params: GltUserTicketLogParam) {
export async function listGltUserTicketLog(params?: GltUserTicketLogParam) { export async function listGltUserTicketLog(params?: GltUserTicketLogParam) {
const res = await request.get<ApiResult<GltUserTicketLog[]>>( const res = await request.get<ApiResult<GltUserTicketLog[]>>(
'/glt/glt-user-ticket-log', '/glt/glt-user-ticket-log',
{ params
params
}
); );
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
return res.data; return res.data;

View File

@@ -50,4 +50,5 @@ export interface GltUserTicketLog {
export interface GltUserTicketLogParam extends PageParam { export interface GltUserTicketLogParam extends PageParam {
id?: number; id?: number;
keywords?: string; keywords?: string;
userId?: number;
} }

View File

@@ -90,11 +90,14 @@ function Home() {
const reload = () => { const reload = () => {
const token = Taro.getStorageSync('access_token') const token = Taro.getStorageSync('access_token')
if (!token) { const userIdRaw = Taro.getStorageSync('UserId')
const userId = Number(userIdRaw)
const hasUserId = Number.isFinite(userId) && userId > 0
if (!token && !hasUserId) {
setTicketTotal(0) setTicketTotal(0)
return return
} }
getMyGltUserTicketTotal() getMyGltUserTicketTotal(hasUserId ? userId : undefined)
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0)) .then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
.catch((err) => { .catch((err) => {
console.error('首页水票总数加载失败:', err) console.error('首页水票总数加载失败:', err)

View File

@@ -29,18 +29,21 @@ const UserCard = forwardRef<any, any>((_, ref) => {
} }
// 下拉刷新 // 下拉刷新
const handleRefresh = async () => { const reloadStats = async (showToast = false) => {
await refresh() await refresh()
reloadTicketTotal() reloadTicketTotal()
Taro.showToast({ if (showToast) {
title: '刷新成功', Taro.showToast({
icon: 'success' title: '刷新成功',
}) icon: 'success'
})
}
} }
// 暴露方法给父组件 // 暴露方法给父组件
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleRefresh handleRefresh: () => reloadStats(true),
reloadStats
})) }))
useEffect(() => { useEffect(() => {
@@ -65,11 +68,14 @@ const UserCard = forwardRef<any, any>((_, ref) => {
const reloadTicketTotal = () => { const reloadTicketTotal = () => {
const token = Taro.getStorageSync('access_token') const token = Taro.getStorageSync('access_token')
if (!token) { const userIdRaw = Taro.getStorageSync('UserId')
const userId = Number(userIdRaw)
const hasUserId = Number.isFinite(userId) && userId > 0
if (!token && !hasUserId) {
setTicketTotal(0) setTicketTotal(0)
return return
} }
getMyGltUserTicketTotal() getMyGltUserTicketTotal(hasUserId ? userId : undefined)
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0)) .then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
.catch((err) => { .catch((err) => {
console.error('个人中心水票总数加载失败:', err) console.error('个人中心水票总数加载失败:', err)
@@ -302,7 +308,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text> <Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
</View> </View>
<View className={'item flex justify-center flex-col items-center'} <View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}> onClick={() => navTo('/user/ticket/index', true)}>
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}></Text> <Text className={'text-xs text-gray-200'} style={themeStyles.textColor}></Text>
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text> <Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
</View> </View>

View File

@@ -8,6 +8,7 @@ import './user.scss'
import IsDealer from "./components/IsDealer"; import IsDealer from "./components/IsDealer";
import {useThemeStyles} from "@/hooks/useTheme"; import {useThemeStyles} from "@/hooks/useTheme";
import UserGrid from "@/pages/user/components/UserGrid"; import UserGrid from "@/pages/user/components/UserGrid";
import { useDidShow } from '@tarojs/taro'
function User() { function User() {
@@ -24,6 +25,11 @@ function User() {
useEffect(() => { useEffect(() => {
}, []); }, []);
// 每次进入/切回个人中心都刷新一次统计(包含水票数量)
useDidShow(() => {
userCardRef.current?.reloadStats?.()
})
return ( return (
<PullToRefresh <PullToRefresh
onRefresh={handleRefresh} onRefresh={handleRefresh}

View File

@@ -156,6 +156,14 @@ const UserTicketList = () => {
const reloadLogs = async (isRefresh = true, keywords?: string) => { const reloadLogs = async (isRefresh = true, keywords?: string) => {
if (logLoading) return; if (logLoading) return;
const userId = getUserId();
if (!userId) {
setLogList([]);
setLogTotal(0);
setLogHasMore(false);
return;
}
if (isRefresh) { if (isRefresh) {
setLogPage(1); setLogPage(1);
setLogList([]); setLogList([]);
@@ -168,6 +176,7 @@ const UserTicketList = () => {
const res = await pageGltUserTicketLog({ const res = await pageGltUserTicketLog({
page: currentPage, page: currentPage,
limit: PAGE_SIZE, limit: PAGE_SIZE,
userId,
keywords: (keywords ?? searchValue) || undefined keywords: (keywords ?? searchValue) || undefined
}); });
@@ -285,11 +294,11 @@ const UserTicketList = () => {
{/* 列表 */} {/* 列表 */}
<PullToRefresh onRefresh={handleRefresh} headHeight={60}> <PullToRefresh onRefresh={handleRefresh} headHeight={60}>
<View style={{ height: 'calc(100vh - 200px)', overflowY: 'auto' }} id="ticket-scroll"> <View style={{ height: 'calc(100vh)', overflowY: 'auto' }} id="ticket-scroll">
{activeTab === 'ticket' && ticketList.length === 0 && !ticketLoading ? ( {activeTab === 'ticket' && ticketList.length === 0 && !ticketLoading ? (
<View <View
className="flex flex-col justify-center items-center" className="flex flex-col justify-center items-center"
style={{ height: 'calc(100vh - 260px)' }} style={{ height: 'calc(100vh - 160px)' }}
> >
<Empty <Empty
description="暂无水票" description="暂无水票"
@@ -299,7 +308,7 @@ const UserTicketList = () => {
) : activeTab === 'log' && logList.length === 0 && !logLoading ? ( ) : activeTab === 'log' && logList.length === 0 && !logLoading ? (
<View <View
className="flex flex-col justify-center items-center" className="flex flex-col justify-center items-center"
style={{ height: 'calc(100vh - 260px)' }} style={{ height: 'calc(100vh - 160px)' }}
> >
<Empty description="暂无核销记录" style={{ backgroundColor: 'transparent' }} /> <Empty description="暂无核销记录" style={{ backgroundColor: 'transparent' }} />
</View> </View>

View File

@@ -182,10 +182,10 @@ const handleAuthError = () => {
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}); });
//
setTimeout(() => { // setTimeout(() => {
Taro.reLaunch({ url: '/passport/login' }); // Taro.reLaunch({ url: '/passport/login' });
}, 2000); // }, 2000);
}; };
// 错误处理 // 错误处理