Files
mini-programs/src/services/userService.ts

840 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { UserInfo } from "@/components/UserInfo";
import { API_CONFIG } from "@/config/api";
import httpService, { ApiResponse } from "./httpService";
import uploadFiles from "./uploadFiles";
import * as Taro from "@tarojs/taro";
import getCurrentConfig from "@/config/env";
import { clear_login_state } from "@/services/loginService";
// 用户详情接口
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;
district: string;
language: string;
phone: string;
is_subscribed: string;
latitude: string;
longitude: string;
subscribe_time: string;
last_login_time: string;
create_time: string;
last_modify_time: string;
personal_profile: string;
occupation: string;
birthday: string;
ntrp_level: string;
last_location_province: string;
last_location_city: string;
stats: {
followers_count: number;
following_count: number;
hosted_games_count: number;
participated_games_count: number;
};
}
export interface PickerOption {
text: string | number;
value: string | number;
children?: PickerOption[];
}
export interface Profession {
name: string;
children: Profession[] | [];
}
// 用户详细信息接口(从 loginService 移过来)
export interface UserInfoType {
id: number;
openid: string;
unionid: string;
session_key: string;
nickname: string;
avatar_url: string;
gender: string;
country: string;
province: string;
city: string;
district: string;
language: string;
phone: string;
is_subscribed: string;
latitude: number;
longitude: number;
subscribe_time: string;
last_login_time: string;
avatar: string;
join_date: string;
stats: {
following_count: number;
followers_count: number;
hosted_games_count: number;
participated_games_count: number;
};
personal_profile: string;
occupation: string;
ntrp_level: string;
last_location_province?: string;
last_location_city?: string;
bio?: string;
birthday?: string;
is_following?: boolean;
tags?: string[];
ongoing_games?: string[];
}
export interface NicknameChangeStatus {
can_change: boolean;
remaining_count: number;
period_start_time: string;
next_period_start_time: string;
days_until_next_period: number;
next_period_start_date?: 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?: string;
longitude?: string;
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;
distance_km: string;
};
participants: {
user: {
avatar_url: string;
};
}[];
}
const formatOptions = (data: Profession[]): PickerOption[] => {
return data.map((item: Profession) => {
const { name: text, children } = item;
const itm: PickerOption = {
text,
value: text,
children: children ? formatOptions(children) : [],
};
if (!itm.children!.length) {
delete itm.children;
}
return itm;
});
};
// 用户服务类
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.replace(/\s/, "T"));
const date_time = this.format_date_time(start_time);
// 处理图片数组 - 兼容两种数据格式
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: number =
typeof game.latitude === "number"
? game.latitude
: parseFloat(game.latitude || "0") || 0;
let longitude: number =
typeof game.longitude === "number"
? game.longitude
: parseFloat(game.longitude || "0") || 0;
if (game.venue_dtl) {
latitude = parseFloat(game.venue_dtl.latitude) || latitude;
longitude = parseFloat(game.venue_dtl.longitude) || 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;
// 转换为 ListCard 期望的格式
return {
id: game.id,
title: game.title || "未命名球局",
start_time: date_time,
original_start_time: game.start_time,
end_time: game.end_time || "",
location: location,
distance_km: game.venue_dtl?.distance_km ,
current_players: registered_count,
max_players: max_count,
skill_level_min: parseInt(game.skill_level_min) || 0,
skill_level_max: parseInt(game.skill_level_max) || 0,
play_type: game.play_type || "不限",
image_list: images,
court_type: game.court_type || "未知",
matchType: game.play_type || "不限",
shinei: game.court_type || "未知",
participants: game.participants || [],
};
});
}
private static is_date_in_this_week(date: Date): boolean {
const today = new Date();
const firstDayOfWeek = new Date(
today.setDate(today.getDate() - today.getDay())
);
const lastDayOfWeek = new Date(
firstDayOfWeek.setDate(firstDayOfWeek.getDate() + 6)
);
return date >= firstDayOfWeek && date <= lastDayOfWeek;
}
// 格式化时间显示
private static format_date_time(start_time: Date): string {
const now = new Date();
const today = new Date(
now.getFullYear(),
now.getMonth() + 1,
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 weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
const start_date = new Date(
start_time.getFullYear(),
start_time.getMonth() + 1,
start_time.getDate()
);
const weekday = weekdays[start_time.getDay()];
let date_str = "";
if (start_date.getTime() === today.getTime()) {
date_str = "今天";
} else if (start_date.getTime() === tomorrow.getTime()) {
date_str = `明天(${weekday})`;
} else if (start_date.getTime() === day_after_tomorrow.getTime()) {
date_str = `后天(${weekday})`;
} else if (this.is_date_in_this_week(start_time)) {
date_str = weekday;
} else {
date_str = `${start_time.getFullYear()}-${(start_time.getMonth() + 1)
.toString()
.padStart(2, "0")}-${start_time
.getDate()
.toString()
.padStart(2, "0")}(${weekday})`;
}
const time_str = `${start_time
.getHours()
.toString()
.padStart(2, "0")}:${start_time
.getMinutes()
.toString()
.padStart(2, "0")}`;
return `${date_str} ${time_str}`;
}
// 获取用户信息
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 } : {},
{
showLoading: false,
}
);
if (response.code === 0) {
const userData = response.data;
return {
id: userData.id || "",
nickname: userData.nickname || "",
avatar: userData.avatar_url || "",
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,
},
personal_profile: userData.personal_profile || "",
occupation: userData.occupation || "",
ntrp_level: userData.ntrp_level || "",
phone: userData.phone || "",
gender: userData.gender || "",
birthday: userData.birthday || "",
country: userData.country || "",
province: userData.province || "",
city: userData.city || "",
last_location_province: userData.last_location_province || "",
last_location_city: userData.last_location_city || "",
};
} else {
throw new Error(response.message || "获取用户信息失败");
}
} catch (error) {
console.error("获取用户信息失败:", error);
// 返回默认用户信息
return {} as UserInfo;
}
}
// 更新用户信息(简化版本,具体逻辑在 userStore 中处理)
static async update_user_info(update_data: Partial<UserInfo>): Promise<void> {
try {
// 过滤掉空字段
const filtered_data: Record<string, any> = {};
Object.keys(update_data).forEach((key) => {
const value = update_data[key as keyof UserInfo];
// 只添加非空且非空字符串的字段
if (value !== null && value !== undefined && value !== "") {
if (typeof value === "string" && value.trim() !== "") {
filtered_data[key] = value.trim();
} else if (typeof value !== "string") {
filtered_data[key] = value;
}
}
});
// 如果没有需要更新的字段,直接返回
if (Object.keys(filtered_data).length === 0) {
console.log("没有需要更新的字段");
return;
}
const response = await httpService.post(
API_CONFIG.USER.UPDATE,
filtered_data,
{
showLoading: true,
}
);
if (response.code !== 0) {
throw new Error(response.message || "更新用户信息失败");
}
} catch (error) {
console.error("更新用户信息失败:", error);
throw error;
}
}
// 获取用户主办的球局
static async get_hosted_games(userId: string | number): Promise<any[]> {
try {
const response = await httpService.post<any>(
API_CONFIG.USER.HOSTED_GAMES,
{
userId,
},
{
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 [];
}
}
// 获取用户参与的球局
static async get_participated_games(userId: string | number): Promise<any[]> {
try {
const response = await httpService.post<any>(
API_CONFIG.USER.PARTICIPATED_GAMES,
{
userId,
},
{
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 [];
}
}
// 获取用户球局记录(兼容旧方法)
static async get_user_games(
user_id: string | number,
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(
following_id: string | number,
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,
{ following_id },
{
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 field_mapping: Record<string, string> = {
nickname: "nickname",
avatar: "avatar_url",
gender: "gender",
phone: "phone",
latitude: "latitude",
longitude: "longitude",
province: "province",
country: "country",
city: "city",
personal_profile: "personal_profile",
occupation: "occupation",
ntrp_level: "ntrp_level",
};
// 构建更新参数,只包含非空字段
const updateParams: Record<string, string> = {};
// 循环处理所有字段
Object.keys(field_mapping).forEach((key) => {
const value = user_info[key as keyof typeof user_info];
if (value && typeof value === "string" && value.trim() !== "") {
updateParams[field_mapping[key]] = value.trim();
}
});
// 如果没有需要更新的字段,直接返回成功
if (Object.keys(updateParams).length === 0) {
console.log("没有需要更新的字段");
return true;
}
const response = await httpService.post<any>(
API_CONFIG.USER.UPDATE,
updateParams,
{
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,
},
{
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 result = await uploadFiles.upload_oss_img(file_path);
await this.save_user_info({ avatar: result.ossPath });
// 使用新的响应格式中的file_url字段
return result.ossPath;
} catch (error) {
console.error("头像上传失败:", error);
// 如果上传失败,返回默认头像
return require("../static/userInfo/default_avatar.svg");
}
}
// 解析用户手机号
static async parse_phone(phone_code: string): Promise<string> {
try {
const response = await httpService.post<{ phone: string }>(
API_CONFIG.USER.PARSE_PHONE,
{ phone_code },
{
showLoading: true,
loadingText: "获取手机号中...",
}
);
if (response.code === 0) {
return response.data.phone || "";
} else {
throw new Error(response.message || "获取手机号失败");
}
} catch (error) {
console.error("获取手机号失败:", error);
return "";
}
}
// 获取职业树
static async getProfessions(): Promise<[] | PickerOption[]> {
try {
const response = await httpService.post<any>(API_CONFIG.PROFESSIONS);
const { code, data, message } = response;
if (code === 0) {
return formatOptions(data || []);
} else {
throw new Error(message || "获取职业树失败");
}
} catch (error) {
console.error("获取职业树失败:", error);
return [];
}
}
// 获取城市树
static async getCities(): Promise<[] | PickerOption[]> {
try {
const response = await httpService.post<any>(API_CONFIG.CITIS);
const { code, data, message } = response;
if (code === 0) {
return formatOptions(data || []);
} else {
throw new Error(message || "获取城市树失败");
}
} catch (error) {
console.error("获取职业树失败:", error);
return [];
}
}
// 注销账户
static async logout(): Promise<void> {
try {
const response = await httpService.post<any>(API_CONFIG.USER.LOGOUT);
const { code, message } = response;
if (code === 0) {
// 清除用户数据
clear_login_state();
Taro.reLaunch({
url: "/login_pages/index/index",
});
} else {
throw new Error(message || "注销账户失败");
}
} catch (error) {
console.error("注销账户失败:", error);
}
}
}
// 从 loginService 移过来的用户相关方法
// 获取用户详细信息
export const fetchUserProfile = async (): Promise<
ApiResponse<UserInfoType>
> => {
try {
const response = await httpService.post("user/detail");
return response;
} catch (error) {
console.error("获取用户信息失败:", error);
throw error;
}
};
// 获取昵称修改状态
export const checkNicknameChangeStatus = async (): Promise<
ApiResponse<NicknameChangeStatus>
> => {
try {
const response = await httpService.post(
"/user/check_nickname_change_status"
);
return response;
} catch (error) {
console.error("获取昵称修改状态失败:", error);
throw error;
}
};
// 修改昵称
export const updateNickname = async (nickname: string) => {
try {
const response = await httpService.post("/user/update_nickname", {
nickname,
});
return response;
} catch (error) {
console.error("昵称修改失败:", error);
throw error;
}
};
// 更新用户信息
export const updateUserProfile = async (payload: Partial<UserInfoType>) => {
try {
const response = await httpService.post("/user/update", payload);
return response;
} catch (error) {
console.error("更新用户信息失败:", error);
throw error;
}
};
// 更新用户坐标位置
export const updateUserLocation = async (
latitude: number,
longitude: number,
force: boolean = false
) => {
try {
const response = await httpService.post("/user/update_location", {
latitude,
longitude,
force,
});
return response;
} catch (error) {
console.error("更新用户坐标位置失败:", error);
throw error;
}
};
// 获取用户信息(从本地存储)
export const get_user_info = (): any | null => {
try {
let userinfo = Taro.getStorageSync("user_info");
if (userinfo) {
return JSON.parse(userinfo);
}
return null;
} catch (error) {
return null;
}
};
// 客服中心处理函数
export const handleCustomerService = async (): Promise<void> => {
try {
// 获取当前环境的客服配置
const config = getCurrentConfig;
const { customerService } = config;
console.log("打开客服中心,配置信息:", customerService);
// 使用微信官方客服能力
await Taro.openCustomerServiceChat({
extInfo: {
url: customerService.serviceUrl,
},
corpId: customerService.corpId,
success: (res) => {
console.log("打开客服成功:", res);
},
fail: (error) => {
console.error("打开客服失败:", error);
// 如果官方客服不可用,显示备用联系方式
showCustomerServiceFallback(customerService);
},
});
} catch (error) {
console.error("客服功能异常:", error);
// 备用方案:显示联系信息
showCustomerServiceFallback();
}
};
// 客服备用方案
const showCustomerServiceFallback = (customerInfo?: any) => {
const options = ["拨打客服电话", "复制邮箱地址"];
// 如果没有客服信息,只显示通用提示
if (!customerInfo?.phoneNumber && !customerInfo?.email) {
Taro.showModal({
title: "联系客服",
content: "如需帮助,请通过其他方式联系我们",
showCancel: false,
});
return;
}
Taro.showActionSheet({
itemList: options,
success: async (res) => {
if (res.tapIndex === 0 && customerInfo?.phoneNumber) {
// 拨打客服电话
try {
await Taro.makePhoneCall({
phoneNumber: customerInfo.phoneNumber,
});
} catch (error) {
console.error("拨打电话失败:", error);
Taro.showToast({
title: "拨打电话失败",
icon: "none",
});
}
} else if (res.tapIndex === 1 && customerInfo?.email) {
// 复制邮箱地址
try {
await Taro.setClipboardData({
data: customerInfo.email,
});
Taro.showToast({
title: "邮箱地址已复制",
icon: "success",
});
} catch (error) {
console.error("复制邮箱失败:", error);
Taro.showToast({
title: "复制失败",
icon: "none",
});
}
}
},
});
};