Merge remote-tracking branch origin into feature/juguohong/20250816

This commit is contained in:
李瑞
2025-09-07 18:56:23 +08:00
46 changed files with 4798 additions and 836 deletions

View File

@@ -20,6 +20,12 @@ export interface GameDetail {
updated_at: string,
}
export enum MATCH_STATUS {
NOT_STARTED = 0, // 未开始
IN_PROGRESS = 1, //进行中
FINISHED = 2 //已结束
}
// 响应接口
export interface Response {
code: string

View File

@@ -353,7 +353,7 @@ export const refresh_login_status = async (): Promise<boolean> => {
// 获取用户详细信息
export const fetchUserProfile = async (): Promise<ApiResponse<UserInfoType>> => {
try {
const response = await httpService.post('/user/detail');
const response = await httpService.post('user/detail');
return response;
} catch (error) {
console.error('获取用户信息失败:', error);
@@ -364,7 +364,7 @@ export const fetchUserProfile = async (): Promise<ApiResponse<UserInfoType>> =>
// 更新用户信息
export const updateUserProfile = async (payload: Partial<UserInfoType>) => {
try {
const response = await httpService.post('user/update', payload);
const response = await httpService.post('/user/update', payload);
return response;
} catch (error) {
console.error('更新用户信息失败:', error);

View File

@@ -0,0 +1,46 @@
import httpService from './httpService'
import type { ApiResponse } from './httpService'
import { requestPayment } from '@tarojs/taro'
export interface SignType {
/** 仅在微信支付 v2 版本接口适用 */
MD5
/** 仅在微信支付 v2 版本接口适用 */
'HMAC-SHA256'
/** 仅在微信支付 v3 版本接口适用 */
RSA
}
export interface PayMentParams {
order_id: number,
order_no: string,
status: number,
appId: string,
timeStamp: string,
nonceStr: string,
package: string,
signType: keyof SignType,
paySign: string
}
// 用户接口
export interface OrderResponse {
participant_id: number,
payment_required: boolean,
payment_params: PayMentParams
}
// 发布球局类
class OrderService {
// 用户登录
async createOrder(game_id: number): Promise<ApiResponse<OrderResponse>> {
return httpService.post('/payment/create_order', { game_id }, {
showLoading: true,
})
}
// async getOrderInfo()
}
// 导出认证服务实例
export default new OrderService()

549
src/services/userService.ts Normal file
View File

@@ -0,0 +1,549 @@
import { UserInfo } from '@/components/UserInfo';
import Taro from '@tarojs/taro';
import { API_CONFIG } from '@/config/api';
import httpService from './httpService';
// 用户详情接口
interface UserDetailData {
id: number;
openid: string;
user_code: string | null;
unionid: string;
session_key: string;
nickname: string;
avatar_url: string;
gender: string;
country: string;
province: string;
city: string;
language: string;
phone: string;
is_subscribed: string;
latitude: number;
longitude: number;
subscribe_time: string;
last_login_time: string;
create_time: string;
last_modify_time: string;
stats: {
followers_count: number;
following_count: number;
hosted_games_count: number;
participated_games_count: number;
};
}
// 更新用户信息参数接口
interface UpdateUserParams {
nickname: string;
avatar_url: string;
gender: string;
phone: string;
latitude: number;
longitude: number;
city: string;
province: string;
country: string;
}
// 上传响应接口
interface UploadResponseData {
create_time: string;
last_modify_time: string;
duration: string;
thumbnail_url: string;
view_count: string;
download_count: string;
is_delete: number;
id: number;
user_id: number;
resource_type: string;
file_name: string;
original_name: string;
file_path: string;
file_url: string;
file_size: number;
mime_type: string;
description: string;
tags: string;
is_public: string;
width: number;
height: number;
uploadInfo: {
success: boolean;
name: string;
path: string;
ossPath: string;
fileType: string;
fileSize: number;
originalName: string;
suffix: string;
storagePath: string;
};
}
// 后端球局数据接口
interface BackendGameData {
id: number;
title: string;
description: string;
game_type?: string;
play_type: string;
publisher_id?: string;
venue_id?: string;
max_players?: number;
current_players?: number;
price: string;
price_mode: string;
court_type: string;
court_surface: string;
gender_limit?: string;
skill_level_min: string;
skill_level_max: string;
start_time: string;
end_time: string;
location_name: string | null;
location: string;
latitude?: number;
longitude?: number;
image_list?: string[];
description_tag?: string[];
venue_description_tag?: string[];
venue_image_list?: Array<{ id: string; url: string }>;
participant_count: number;
max_participants: number;
participant_info?: {
id: number;
status: string;
payment_status: string;
joined_at: string;
deposit_amount: number;
join_message: string;
skill_level: string;
contact_info: string;
};
venue_dtl?: {
id: number;
name: string;
address: string;
latitude: string;
longitude: string;
venue_type: string;
surface_type: string;
};
}
// 用户服务类
export class UserService {
// 数据转换函数将后端数据转换为ListContainer期望的格式
private static transform_game_data(backend_data: BackendGameData[]): any[] {
return backend_data.map(game => {
// 处理时间格式
const start_time = new Date(game.start_time);
const date_time = this.format_date_time(start_time);
// 处理技能等级
const skill_level = this.format_skill_level(game.skill_level_min, game.skill_level_max);
// 处理图片数组 - 兼容两种数据格式
let images: string[] = [];
if (game.image_list && game.image_list.length > 0) {
images = game.image_list.filter(img => img && img.trim() !== '');
} else if (game.venue_image_list && game.venue_image_list.length > 0) {
images = game.venue_image_list
.filter(img => img && img.url && img.url.trim() !== '')
.map(img => img.url);
}
// 处理距离 - 优先使用venue_dtl中的坐标其次使用game中的坐标
let latitude = game.latitude || 0;
let longitude = game.longitude || 0;
if (game.venue_dtl) {
latitude = parseFloat(game.venue_dtl.latitude) || latitude;
longitude = parseFloat(game.venue_dtl.longitude) || longitude;
}
const distance = this.calculate_distance(latitude, longitude);
// 处理地点信息 - 优先使用venue_dtl中的信息
let location = game.location_name || game.location || '未知地点';
if (game.venue_dtl && game.venue_dtl.name) {
location = game.venue_dtl.name;
}
// 处理人数统计 - 兼容不同的字段名
const registered_count = game.current_players || game.participant_count || 0;
const max_count = game.max_players || game.max_participants || 0;
return {
id: game.id,
title: game.title || '未命名球局',
dateTime: date_time,
location: location,
distance: distance,
registeredCount: registered_count,
maxCount: max_count,
skillLevel: skill_level,
matchType: game.play_type || '不限',
images: images,
shinei: game.court_type || '未知'
};
});
}
// 格式化时间显示
private static format_date_time(start_time: Date): string {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
const day_after_tomorrow = new Date(today.getTime() + 2 * 24 * 60 * 60 * 1000);
const start_date = new Date(start_time.getFullYear(), start_time.getMonth(), start_time.getDate());
let date_str = '';
if (start_date.getTime() === today.getTime()) {
date_str = '今天';
} else if (start_date.getTime() === tomorrow.getTime()) {
date_str = '明天';
} else if (start_date.getTime() === day_after_tomorrow.getTime()) {
date_str = '后天';
} else {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
date_str = weekdays[start_time.getDay()];
}
const time_str = `${start_time.getHours().toString().padStart(2, '0')}:${start_time.getMinutes().toString().padStart(2, '0')}`;
return `${date_str} ${time_str}`;
}
// 格式化技能等级
private static format_skill_level(min: string, max: string): string {
const min_num = parseInt(min) || 0;
const max_num = parseInt(max) || 0;
if (min_num === 0 && max_num === 0) {
return '不限';
}
if (min_num === max_num) {
return `${min_num}.0`;
}
return `${min_num}.0-${max_num}.0`;
}
// 计算距离(模拟实现,实际需要根据用户位置计算)
private static calculate_distance(latitude: number, longitude: number): string {
if (latitude === 0 && longitude === 0) {
return '未知距离';
}
// 这里应该根据用户当前位置计算实际距离
// 暂时返回模拟距离
const distances = ['1.2km', '2.5km', '3.8km', '5.1km', '7.3km'];
return distances[Math.floor(Math.random() * distances.length)];
}
// 获取用户信息
static async get_user_info(user_id?: string): Promise<UserInfo> {
try {
const response = await httpService.post<UserDetailData>(API_CONFIG.USER.DETAIL, user_id ? { user_id } : {}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
const userData = response.data;
return {
id: userData.user_code || user_id || '1',
nickname: userData.nickname || '用户',
avatar: userData.avatar_url || require('../static/userInfo/default_avatar.svg'),
join_date: userData.subscribe_time ? `${new Date(userData.subscribe_time).getFullYear()}${new Date(userData.subscribe_time).getMonth() + 1}月加入` : '未知时间加入',
stats: {
following: userData.stats?.following_count || 0,
friends: userData.stats?.followers_count || 0,
hosted: userData.stats?.hosted_games_count || 0,
participated: userData.stats?.participated_games_count || 0
},
tags: [
userData.city || '未知地区',
userData.province || '未知省份',
'NTRP 3.0' // 默认等级,需要其他接口获取
],
bio: '这个人很懒,什么都没有写...',
location: userData.city || '未知地区',
occupation: '未知职业', // 需要其他接口获取
ntrp_level: 'NTRP 3.0', // 需要其他接口获取
phone: userData.phone || '',
gender: userData.gender || ''
};
} else {
throw new Error(response.message || '获取用户信息失败');
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 返回默认用户信息
return {
id: user_id || '1',
nickname: '用户',
avatar: require('../static/userInfo/default_avatar.svg'),
join_date: '未知时间加入',
stats: {
following: 0,
friends: 0,
hosted: 0,
participated: 0
},
tags: ['未知地区', '未知职业', 'NTRP 3.0'],
bio: '这个人很懒,什么都没有写...',
location: '未知地区',
occupation: '未知职业',
ntrp_level: 'NTRP 3.0',
phone: '',
gender: ''
};
}
}
// 获取用户主办的球局
static async get_hosted_games(user_id: string): Promise<any[]> {
try {
const response = await httpService.post<any>(API_CONFIG.USER.HOSTED_GAMES, {
user_id
}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
// 使用数据转换函数将后端数据转换为ListContainer期望的格式
return this.transform_game_data(response.data.rows || []);
} else {
throw new Error(response.message || '获取主办球局失败');
}
} catch (error) {
console.error('获取主办球局失败:', error);
// 返回符合ListContainer data格式的模拟数据
return [
{
id: 1,
title: '女生轻松双打',
dateTime: '明天(周五) 下午5点',
location: '仁恒河滨花园网球场',
distance: '3.5km',
registeredCount: 2,
maxCount: 4,
skillLevel: '2.0-2.5',
matchType: '双打',
images: [
require('../static/userInfo/game1.svg'),
require('../static/userInfo/game2.svg'),
require('../static/userInfo/game3.svg')
],
shinei: '室外'
},
{
id: 5,
title: '新手友好局',
dateTime: '周日 下午2点',
location: '徐汇网球中心',
distance: '1.8km',
registeredCount: 4,
maxCount: 6,
skillLevel: '1.5-2.0',
matchType: '双打',
images: [
require('../static/userInfo/game1.svg'),
require('../static/userInfo/game2.svg')
],
shinei: '室外'
}
];
}
}
// 获取用户参与的球局
static async get_participated_games(user_id: string): Promise<any[]> {
try {
const response = await httpService.post<any>(API_CONFIG.USER.PARTICIPATED_GAMES, {
user_id
}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
// 使用数据转换函数将后端数据转换为ListContainer期望的格式
return this.transform_game_data(response.data.rows || []);
} else {
throw new Error(response.message || '获取参与球局失败');
}
} catch (error) {
console.error('获取参与球局失败:', error);
// 返回符合ListContainer data格式的模拟数据
return [
{
id: 2,
title: '周末双打练习',
dateTime: '后天(周六) 上午10点',
location: '上海网球中心',
distance: '5.2km',
registeredCount: 6,
maxCount: 8,
skillLevel: '3.0-3.5',
matchType: '双打',
images: [
require('../static/userInfo/game2.svg'),
require('../static/userInfo/game3.svg')
],
shinei: '室内'
},
{
id: 3,
title: '晨练单打',
dateTime: '明天(周五) 早上7点',
location: '浦东网球俱乐部',
distance: '2.8km',
registeredCount: 1,
maxCount: 2,
skillLevel: '2.5-3.0',
matchType: '单打',
images: [
require('../static/userInfo/game1.svg')
],
shinei: '室外'
},
{
id: 4,
title: '夜场混双',
dateTime: '今晚 晚上8点',
location: '虹桥网球中心',
distance: '4.1km',
registeredCount: 3,
maxCount: 4,
skillLevel: '3.5-4.0',
matchType: '混双',
images: [
require('../static/userInfo/game1.svg'),
require('../static/userInfo/game2.svg'),
require('../static/userInfo/game3.svg')
],
shinei: '室内'
}
];
}
}
// 获取用户球局记录(兼容旧方法)
static async get_user_games(user_id: string, type: 'hosted' | 'participated'): Promise<any[]> {
if (type === 'hosted') {
return this.get_hosted_games(user_id);
} else {
return this.get_participated_games(user_id);
}
}
// 关注/取消关注用户
static async toggle_follow(user_id: string, is_following: boolean): Promise<boolean> {
try {
const endpoint = is_following ? API_CONFIG.USER.UNFOLLOW : API_CONFIG.USER.FOLLOW;
const response = await httpService.post<any>(endpoint, { user_id }, {
needAuth: false,
showLoading: true,
loadingText: is_following ? '取消关注中...' : '关注中...'
});
if (response.code === 0) {
return !is_following;
} else {
throw new Error(response.message || '操作失败');
}
} catch (error) {
console.error('关注操作失败:', error);
throw error;
}
}
// 保存用户信息
static async save_user_info(user_info: Partial<UserInfo> & { phone?: string; gender?: string }): Promise<boolean> {
try {
// 获取当前位置信息
const location = await Taro.getLocation({
type: 'wgs84'
});
const updateParams: UpdateUserParams = {
nickname: user_info.nickname || '',
avatar_url: user_info.avatar || '',
gender: user_info.gender || '',
phone: user_info.phone || '',
latitude: location.latitude,
longitude: location.longitude,
city: user_info.location || '',
province: '', // 需要从用户信息中获取
country: '' // 需要从用户信息中获取
};
const response = await httpService.post<any>(API_CONFIG.USER.UPDATE, updateParams, {
needAuth: false,
showLoading: true,
loadingText: '保存中...'
});
if (response.code === 0) {
return true;
} else {
throw new Error(response.message || '更新用户信息失败');
}
} catch (error) {
console.error('保存用户信息失败:', error);
throw error;
}
}
// 获取用户动态
static async get_user_activities(user_id: string, page: number = 1, limit: number = 10): Promise<any[]> {
try {
const response = await httpService.post<any>('/user/activities', {
user_id,
page,
limit
}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
return response.data.activities || [];
} else {
throw new Error(response.message || '获取用户动态失败');
}
} catch (error) {
console.error('获取用户动态失败:', error);
return [];
}
}
// 上传头像
static async upload_avatar(file_path: string): Promise<string> {
try {
// 先上传文件到服务器
const uploadResponse = await Taro.uploadFile({
url: `${API_CONFIG.BASE_URL}${API_CONFIG.UPLOAD.AVATAR}`,
filePath: file_path,
name: 'file'
});
const result = JSON.parse(uploadResponse.data) as { code: number; message: string; data: UploadResponseData };
if (result.code === 0) {
// 使用新的响应格式中的file_url字段
return result.data.file_url;
} else {
throw new Error(result.message || '头像上传失败');
}
} catch (error) {
console.error('头像上传失败:', error);
// 如果上传失败,返回默认头像
return require('../static/userInfo/default_avatar.svg');
}
}
}