資料庫改為 mySQL
This commit is contained in:
669
lib/database-service.ts
Normal file
669
lib/database-service.ts
Normal file
@@ -0,0 +1,669 @@
|
||||
// 統一的資料庫服務層 - 支援 Supabase 和 MySQL 切換
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import type { ImageFile } from "./image-utils"
|
||||
|
||||
// 確保 PrismaClient 正確初始化
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// 資料庫類型枚舉
|
||||
export type DatabaseType = 'supabase' | 'mysql'
|
||||
|
||||
// 配置
|
||||
const DATABASE_TYPE: DatabaseType = (process.env.DATABASE_TYPE as DatabaseType) || 'mysql'
|
||||
|
||||
// Supabase 配置
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
|
||||
// 創建客戶端(只有在有 Supabase 環境變數時才創建)
|
||||
const supabase = supabaseUrl && supabaseAnonKey ? createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
},
|
||||
db: {
|
||||
schema: "public",
|
||||
},
|
||||
}) : null
|
||||
|
||||
// 使用上面定義的 prisma 實例
|
||||
|
||||
// 類型定義
|
||||
export interface Wish {
|
||||
id: number
|
||||
title: string
|
||||
current_pain: string
|
||||
expected_solution: string
|
||||
expected_effect: string | null
|
||||
is_public: boolean
|
||||
email: string | null
|
||||
images: any[] | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
like_count?: number
|
||||
}
|
||||
|
||||
export interface WishInsert {
|
||||
title: string
|
||||
current_pain: string
|
||||
expected_solution: string
|
||||
expected_effect?: string | null
|
||||
is_public?: boolean
|
||||
email?: string | null
|
||||
images?: any[] | null
|
||||
}
|
||||
|
||||
export interface WishLike {
|
||||
id: number
|
||||
wish_id: number
|
||||
user_session: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
id: number
|
||||
user_session: string
|
||||
background_music_enabled: boolean
|
||||
background_music_volume: number
|
||||
background_music_playing: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 錯誤處理
|
||||
export class DatabaseError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public originalError?: any,
|
||||
) {
|
||||
super(message)
|
||||
this.name = "DatabaseError"
|
||||
}
|
||||
}
|
||||
|
||||
// 生成用戶會話 ID
|
||||
export function getUserSession(): string {
|
||||
if (typeof window === "undefined") return "server-session"
|
||||
|
||||
let session = localStorage.getItem("user_session")
|
||||
if (!session) {
|
||||
session = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
localStorage.setItem("user_session", session)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
// 困擾案例服務
|
||||
export class WishService {
|
||||
// 獲取所有公開的困擾案例
|
||||
static async getPublicWishes(): Promise<Wish[]> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.getPublicWishesFromSupabase()
|
||||
} else {
|
||||
return await this.getPublicWishesFromMySQL()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching public wishes:", error)
|
||||
throw new DatabaseError("獲取公開困擾失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取所有困擾案例
|
||||
static async getAllWishes(): Promise<Wish[]> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.getAllWishesFromSupabase()
|
||||
} else {
|
||||
return await this.getAllWishesFromMySQL()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching all wishes:", error)
|
||||
throw new DatabaseError("獲取所有困擾失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 創建困擾案例
|
||||
static async createWish(wishData: {
|
||||
title: string
|
||||
currentPain: string
|
||||
expectedSolution: string
|
||||
expectedEffect?: string
|
||||
isPublic?: boolean
|
||||
email?: string
|
||||
images?: ImageFile[]
|
||||
}): Promise<Wish> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.createWishInSupabase(wishData)
|
||||
} else {
|
||||
return await this.createWishInMySQL(wishData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating wish:", error)
|
||||
throw new DatabaseError("創建困擾失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取統計數據
|
||||
static async getWishesStats() {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.getWishesStatsFromSupabase()
|
||||
} else {
|
||||
return await this.getWishesStatsFromMySQL()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching wishes stats:", error)
|
||||
throw new DatabaseError("獲取統計數據失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 實現
|
||||
private static async getPublicWishesFromSupabase(): Promise<Wish[]> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const { data, error } = await supabase
|
||||
.from("wishes_with_likes")
|
||||
.select("*")
|
||||
.eq("is_public", true)
|
||||
.order("created_at", { ascending: false })
|
||||
|
||||
if (error) throw new DatabaseError("獲取公開困擾失敗", error)
|
||||
return data || []
|
||||
}
|
||||
|
||||
private static async getAllWishesFromSupabase(): Promise<Wish[]> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const { data, error } = await supabase
|
||||
.from("wishes_with_likes")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false })
|
||||
|
||||
if (error) throw new DatabaseError("獲取所有困擾失敗", error)
|
||||
return data || []
|
||||
}
|
||||
|
||||
private static async createWishInSupabase(wishData: any): Promise<Wish> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const imageData = wishData.images?.map((img: ImageFile) => ({
|
||||
id: img.id,
|
||||
name: img.name,
|
||||
size: img.size,
|
||||
type: img.type,
|
||||
base64: img.base64 || img.url,
|
||||
})) || []
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("wishes")
|
||||
.insert({
|
||||
title: wishData.title,
|
||||
current_pain: wishData.currentPain,
|
||||
expected_solution: wishData.expectedSolution,
|
||||
expected_effect: wishData.expectedEffect || null,
|
||||
is_public: wishData.isPublic ?? true,
|
||||
email: wishData.email || null,
|
||||
images: imageData,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw new DatabaseError("創建困擾失敗", error)
|
||||
return data
|
||||
}
|
||||
|
||||
private static async getWishesStatsFromSupabase() {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const { data, error } = await supabase.rpc("get_wishes_stats")
|
||||
if (error) throw new DatabaseError("獲取統計數據失敗", error)
|
||||
return data
|
||||
}
|
||||
|
||||
// MySQL 實現
|
||||
private static async getPublicWishesFromMySQL(): Promise<Wish[]> {
|
||||
const wishes = await prisma.wish.findMany({
|
||||
where: {
|
||||
isPublic: true,
|
||||
status: 'active'
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
return wishes.map((wish: any) => ({
|
||||
...wish,
|
||||
like_count: wish.likes.length,
|
||||
created_at: wish.createdAt.toISOString(),
|
||||
updated_at: wish.updatedAt.toISOString()
|
||||
}))
|
||||
}
|
||||
|
||||
private static async getAllWishesFromMySQL(): Promise<Wish[]> {
|
||||
const wishes = await prisma.wish.findMany({
|
||||
where: {
|
||||
status: 'active'
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
return wishes.map((wish: any) => ({
|
||||
...wish,
|
||||
like_count: wish.likes.length,
|
||||
created_at: wish.createdAt.toISOString(),
|
||||
updated_at: wish.updatedAt.toISOString()
|
||||
}))
|
||||
}
|
||||
|
||||
private static async createWishInMySQL(wishData: any): Promise<Wish> {
|
||||
const imageData = wishData.images?.map((img: ImageFile) => ({
|
||||
id: img.id,
|
||||
name: img.name,
|
||||
size: img.size,
|
||||
type: img.type,
|
||||
base64: img.base64 || img.url,
|
||||
})) || []
|
||||
|
||||
const userSession = getUserSession()
|
||||
|
||||
const wish = await prisma.wish.create({
|
||||
data: {
|
||||
title: wishData.title,
|
||||
currentPain: wishData.currentPain,
|
||||
expectedSolution: wishData.expectedSolution,
|
||||
expectedEffect: wishData.expectedEffect || null,
|
||||
isPublic: wishData.isPublic ?? true,
|
||||
email: wishData.email || null,
|
||||
images: imageData,
|
||||
userSession: userSession,
|
||||
status: 'active',
|
||||
priority: 3
|
||||
},
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...wish,
|
||||
like_count: wish.likes.length,
|
||||
created_at: wish.createdAt.toISOString(),
|
||||
updated_at: wish.updatedAt.toISOString(),
|
||||
current_pain: wish.currentPain,
|
||||
expected_solution: wish.expectedSolution,
|
||||
expected_effect: wish.expectedEffect,
|
||||
is_public: wish.isPublic
|
||||
}
|
||||
}
|
||||
|
||||
private static async getWishesStatsFromMySQL() {
|
||||
const result = await prisma.$queryRaw`
|
||||
CALL GetWishesStats()
|
||||
`
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 點讚服務
|
||||
export class LikeService {
|
||||
// 為困擾案例點讚
|
||||
static async likeWish(wishId: number): Promise<boolean> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.likeWishInSupabase(wishId)
|
||||
} else {
|
||||
return await this.likeWishInMySQL(wishId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error liking wish:", error)
|
||||
throw new DatabaseError("點讚失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 檢查用戶是否已點讚
|
||||
static async hasUserLiked(wishId: number): Promise<boolean> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.hasUserLikedInSupabase(wishId)
|
||||
} else {
|
||||
return await this.hasUserLikedInMySQL(wishId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking like status:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取困擾案例的點讚數
|
||||
static async getWishLikeCount(wishId: number): Promise<number> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.getWishLikeCountFromSupabase(wishId)
|
||||
} else {
|
||||
return await this.getWishLikeCountFromMySQL(wishId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching like count:", error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取用戶已點讚的困擾 ID 列表
|
||||
static async getUserLikedWishes(): Promise<number[]> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.getUserLikedWishesFromSupabase()
|
||||
} else {
|
||||
return await this.getUserLikedWishesFromMySQL()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user liked wishes:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 實現
|
||||
private static async likeWishInSupabase(wishId: number): Promise<boolean> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const userSession = getUserSession()
|
||||
const { error } = await supabase.from("wish_likes").insert({
|
||||
wish_id: wishId,
|
||||
user_session: userSession,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
if (error.code === "23505") return false
|
||||
throw new DatabaseError("點讚失敗", error)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private static async hasUserLikedInSupabase(wishId: number): Promise<boolean> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const userSession = getUserSession()
|
||||
const { data, error } = await supabase
|
||||
.from("wish_likes")
|
||||
.select("id")
|
||||
.eq("wish_id", wishId)
|
||||
.eq("user_session", userSession)
|
||||
.single()
|
||||
|
||||
if (error && error.code !== "PGRST116") {
|
||||
throw new DatabaseError("檢查點讚狀態失敗", error)
|
||||
}
|
||||
return !!data
|
||||
}
|
||||
|
||||
private static async getWishLikeCountFromSupabase(wishId: number): Promise<number> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const { count, error } = await supabase
|
||||
.from("wish_likes")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("wish_id", wishId)
|
||||
|
||||
if (error) throw new DatabaseError("獲取點讚數失敗", error)
|
||||
return count || 0
|
||||
}
|
||||
|
||||
private static async getUserLikedWishesFromSupabase(): Promise<number[]> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const userSession = getUserSession()
|
||||
const { data, error } = await supabase
|
||||
.from("wish_likes")
|
||||
.select("wish_id")
|
||||
.eq("user_session", userSession)
|
||||
|
||||
if (error) throw new DatabaseError("獲取用戶點讚記錄失敗", error)
|
||||
return data?.map((item) => item.wish_id) || []
|
||||
}
|
||||
|
||||
// MySQL 實現
|
||||
private static async likeWishInMySQL(wishId: number): Promise<boolean> {
|
||||
const userSession = getUserSession()
|
||||
try {
|
||||
await prisma.wishLike.create({
|
||||
data: {
|
||||
wishId: wishId,
|
||||
userSession: userSession,
|
||||
}
|
||||
})
|
||||
return true
|
||||
} catch (error: any) {
|
||||
if (error.code === 'P2002') return false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private static async hasUserLikedInMySQL(wishId: number): Promise<boolean> {
|
||||
const userSession = getUserSession()
|
||||
const like = await prisma.wishLike.findFirst({
|
||||
where: {
|
||||
wishId: wishId,
|
||||
userSession: userSession
|
||||
}
|
||||
})
|
||||
return !!like
|
||||
}
|
||||
|
||||
private static async getWishLikeCountFromMySQL(wishId: number): Promise<number> {
|
||||
const count = await prisma.wishLike.count({
|
||||
where: {
|
||||
wishId: wishId
|
||||
}
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
private static async getUserLikedWishesFromMySQL(): Promise<number[]> {
|
||||
const userSession = getUserSession()
|
||||
const likes = await prisma.wishLike.findMany({
|
||||
where: {
|
||||
userSession: userSession
|
||||
},
|
||||
select: {
|
||||
wishId: true
|
||||
}
|
||||
})
|
||||
return likes.map((like: any) => like.wishId)
|
||||
}
|
||||
}
|
||||
|
||||
// 用戶設定服務
|
||||
export class UserSettingsService {
|
||||
// 獲取用戶設定
|
||||
static async getUserSettings(): Promise<UserSettings | null> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.getUserSettingsFromSupabase()
|
||||
} else {
|
||||
return await this.getUserSettingsFromMySQL()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user settings:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 更新或創建用戶設定
|
||||
static async updateUserSettings(settings: {
|
||||
backgroundMusicEnabled?: boolean
|
||||
backgroundMusicVolume?: number
|
||||
backgroundMusicPlaying?: boolean
|
||||
}): Promise<UserSettings> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
return await this.updateUserSettingsInSupabase(settings)
|
||||
} else {
|
||||
return await this.updateUserSettingsInMySQL(settings)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating user settings:", error)
|
||||
throw new DatabaseError("更新用戶設定失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 實現
|
||||
private static async getUserSettingsFromSupabase(): Promise<UserSettings | null> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const userSession = getUserSession()
|
||||
const { data, error } = await supabase
|
||||
.from("user_settings")
|
||||
.select("*")
|
||||
.eq("user_session", userSession)
|
||||
.single()
|
||||
|
||||
if (error && error.code !== "PGRST116") {
|
||||
throw new DatabaseError("獲取用戶設定失敗", error)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private static async updateUserSettingsInSupabase(settings: any): Promise<UserSettings> {
|
||||
if (!supabase) {
|
||||
throw new DatabaseError("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const userSession = getUserSession()
|
||||
const { data: updateData, error: updateError } = await supabase
|
||||
.from("user_settings")
|
||||
.update({
|
||||
background_music_enabled: settings.backgroundMusicEnabled,
|
||||
background_music_volume: settings.backgroundMusicVolume,
|
||||
background_music_playing: settings.backgroundMusicPlaying,
|
||||
})
|
||||
.eq("user_session", userSession)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (updateError && updateError.code === "PGRST116") {
|
||||
const { data: insertData, error: insertError } = await supabase
|
||||
.from("user_settings")
|
||||
.insert({
|
||||
user_session: userSession,
|
||||
background_music_enabled: settings.backgroundMusicEnabled ?? false,
|
||||
background_music_volume: settings.backgroundMusicVolume ?? 0.3,
|
||||
background_music_playing: settings.backgroundMusicPlaying ?? false,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (insertError) throw new DatabaseError("創建用戶設定失敗", insertError)
|
||||
return insertData
|
||||
}
|
||||
|
||||
if (updateError) throw new DatabaseError("更新用戶設定失敗", updateError)
|
||||
return updateData
|
||||
}
|
||||
|
||||
// MySQL 實現
|
||||
private static async getUserSettingsFromMySQL(): Promise<UserSettings | null> {
|
||||
const userSession = getUserSession()
|
||||
const settings = await prisma.userSetting.findUnique({
|
||||
where: {
|
||||
userSession: userSession
|
||||
}
|
||||
})
|
||||
|
||||
if (!settings) return null
|
||||
|
||||
return {
|
||||
...settings,
|
||||
created_at: settings.createdAt.toISOString(),
|
||||
updated_at: settings.updatedAt.toISOString(),
|
||||
user_session: settings.userSession,
|
||||
background_music_enabled: settings.backgroundMusicEnabled,
|
||||
background_music_volume: settings.backgroundMusicVolume,
|
||||
background_music_playing: settings.backgroundMusicPlaying
|
||||
}
|
||||
}
|
||||
|
||||
private static async updateUserSettingsInMySQL(settings: any): Promise<UserSettings> {
|
||||
const userSession = getUserSession()
|
||||
const userSettings = await prisma.userSetting.upsert({
|
||||
where: {
|
||||
userSession: userSession
|
||||
},
|
||||
update: {
|
||||
backgroundMusicEnabled: settings.backgroundMusicEnabled,
|
||||
backgroundMusicVolume: settings.backgroundMusicVolume,
|
||||
backgroundMusicPlaying: settings.backgroundMusicPlaying,
|
||||
},
|
||||
create: {
|
||||
userSession: userSession,
|
||||
backgroundMusicEnabled: settings.backgroundMusicEnabled ?? false,
|
||||
backgroundMusicVolume: settings.backgroundMusicVolume ?? 0.3,
|
||||
backgroundMusicPlaying: settings.backgroundMusicPlaying ?? false,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...userSettings,
|
||||
created_at: userSettings.createdAt.toISOString(),
|
||||
updated_at: userSettings.updatedAt.toISOString(),
|
||||
user_session: userSettings.userSession,
|
||||
background_music_enabled: userSettings.backgroundMusicEnabled,
|
||||
background_music_volume: userSettings.backgroundMusicVolume,
|
||||
background_music_playing: userSettings.backgroundMusicPlaying
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 測試資料庫連接
|
||||
export async function testDatabaseConnection(): Promise<boolean> {
|
||||
try {
|
||||
if (DATABASE_TYPE === 'supabase') {
|
||||
if (!supabase) {
|
||||
throw new Error("Supabase 未配置,請使用 MySQL 資料庫")
|
||||
}
|
||||
const { data, error } = await supabase.from("wishes").select("count").limit(1)
|
||||
if (error) throw error
|
||||
console.log("✅ Supabase connection successful")
|
||||
} else {
|
||||
await prisma.$queryRaw`SELECT 1`
|
||||
console.log("✅ MySQL connection successful")
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Database connection test failed:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 關閉資料庫連接
|
||||
export async function closeDatabaseConnection(): Promise<void> {
|
||||
if (DATABASE_TYPE === 'mysql') {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
WishService,
|
||||
LikeService,
|
||||
UserSettingsService,
|
||||
testDatabaseConnection,
|
||||
closeDatabaseConnection
|
||||
}
|
||||
516
lib/mysql-service.ts
Normal file
516
lib/mysql-service.ts
Normal file
@@ -0,0 +1,516 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import type { ImageFile } from './image-utils'
|
||||
import { StatisticsService } from './statistics-service'
|
||||
|
||||
// 創建 Prisma 客戶端實例
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// 類型定義
|
||||
export type Wish = {
|
||||
id: number
|
||||
title: string
|
||||
current_pain: string
|
||||
expected_solution: string
|
||||
expected_effect: string | null
|
||||
is_public: boolean
|
||||
email: string | null
|
||||
images: any[] | null
|
||||
user_session: string
|
||||
status: string
|
||||
category: string | null
|
||||
priority: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
like_count?: number
|
||||
}
|
||||
|
||||
export type WishInsert = {
|
||||
title: string
|
||||
current_pain: string
|
||||
expected_solution: string
|
||||
expected_effect?: string | null
|
||||
is_public?: boolean
|
||||
email?: string | null
|
||||
images?: any[] | null
|
||||
user_session: string
|
||||
status?: string
|
||||
category?: string | null
|
||||
priority?: number
|
||||
}
|
||||
|
||||
export type WishLike = {
|
||||
id: number
|
||||
wish_id: number
|
||||
user_session: string
|
||||
ip_address: string | null
|
||||
user_agent: string | null
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
export type UserSettings = {
|
||||
id: number
|
||||
user_session: string
|
||||
background_music_enabled: boolean
|
||||
background_music_volume: number
|
||||
background_music_playing: boolean
|
||||
theme_preference: string
|
||||
language_preference: string
|
||||
notification_enabled: boolean
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
// 錯誤處理
|
||||
export class MySQLServiceError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public originalError?: any,
|
||||
) {
|
||||
super(message)
|
||||
this.name = "MySQLServiceError"
|
||||
}
|
||||
}
|
||||
|
||||
// 生成用戶會話 ID(用於匿名識別)
|
||||
export function getUserSession(): string {
|
||||
if (typeof window === "undefined") return "server-session"
|
||||
|
||||
let session = localStorage.getItem("user_session")
|
||||
if (!session) {
|
||||
session = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
localStorage.setItem("user_session", session)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
// 困擾案例相關服務
|
||||
export class WishService {
|
||||
// 獲取所有公開的困擾案例(帶點讚數)
|
||||
static async getPublicWishes(): Promise<Wish[]> {
|
||||
try {
|
||||
const wishes = await prisma.wish.findMany({
|
||||
where: {
|
||||
isPublic: true,
|
||||
status: 'active'
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
// 添加點讚數
|
||||
return wishes.map((wish: any) => ({
|
||||
...wish,
|
||||
like_count: wish.likes.length
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error("Error fetching public wishes:", error)
|
||||
throw new MySQLServiceError("獲取公開困擾失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取所有困擾案例(用於分析,包含私密的)
|
||||
static async getAllWishes(): Promise<Wish[]> {
|
||||
try {
|
||||
const wishes = await prisma.wish.findMany({
|
||||
where: {
|
||||
status: 'active'
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
// 添加點讚數
|
||||
return wishes.map((wish: any) => ({
|
||||
...wish,
|
||||
like_count: wish.likes.length
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error("Error fetching all wishes:", error)
|
||||
throw new MySQLServiceError("獲取所有困擾失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 創建新的困擾案例
|
||||
static async createWish(wishData: {
|
||||
title: string
|
||||
currentPain: string
|
||||
expectedSolution: string
|
||||
expectedEffect?: string
|
||||
isPublic?: boolean
|
||||
email?: string
|
||||
images?: ImageFile[]
|
||||
}): Promise<Wish> {
|
||||
try {
|
||||
// 轉換圖片數據格式
|
||||
const imageData = wishData.images?.map((img) => ({
|
||||
id: img.id,
|
||||
name: img.name,
|
||||
size: img.size,
|
||||
type: img.type,
|
||||
base64: img.base64 || img.url,
|
||||
})) || []
|
||||
|
||||
const userSession = getUserSession()
|
||||
|
||||
const wish = await prisma.wish.create({
|
||||
data: {
|
||||
title: wishData.title,
|
||||
currentPain: wishData.currentPain,
|
||||
expectedSolution: wishData.expectedSolution,
|
||||
expectedEffect: wishData.expectedEffect || null,
|
||||
isPublic: wishData.isPublic ?? true,
|
||||
email: wishData.email || null,
|
||||
images: imageData,
|
||||
userSession: userSession,
|
||||
status: 'active',
|
||||
priority: 3
|
||||
},
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
// 更新統計數據
|
||||
await StatisticsService.updateWishStats(Number(wish.id), 'create', wishData.isPublic ?? true)
|
||||
|
||||
return {
|
||||
...wish,
|
||||
like_count: wish.likes.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating wish:", error)
|
||||
throw new MySQLServiceError("創建困擾失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取統計數據
|
||||
static async getWishesStats() {
|
||||
try {
|
||||
const result = await prisma.$queryRaw`
|
||||
CALL GetWishesStats()
|
||||
`
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error("Error fetching wishes stats:", error)
|
||||
throw new MySQLServiceError("獲取統計數據失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 根據 ID 獲取困擾案例
|
||||
static async getWishById(id: number): Promise<Wish | null> {
|
||||
try {
|
||||
const wish = await prisma.wish.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!wish) return null
|
||||
|
||||
return {
|
||||
...wish,
|
||||
like_count: wish.likes.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching wish by id:", error)
|
||||
throw new MySQLServiceError("獲取困擾案例失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新困擾案例
|
||||
static async updateWish(id: number, data: Partial<WishInsert>): Promise<Wish> {
|
||||
try {
|
||||
const wish = await prisma.wish.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
include: {
|
||||
likes: true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...wish,
|
||||
like_count: wish.likes.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating wish:", error)
|
||||
throw new MySQLServiceError("更新困擾案例失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刪除困擾案例
|
||||
static async deleteWish(id: number): Promise<boolean> {
|
||||
try {
|
||||
await prisma.wish.delete({
|
||||
where: { id }
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Error deleting wish:", error)
|
||||
throw new MySQLServiceError("刪除困擾案例失敗", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 點讚相關服務
|
||||
export class LikeService {
|
||||
// 為困擾案例點讚
|
||||
static async likeWish(wishId: number): Promise<boolean> {
|
||||
try {
|
||||
const userSession = getUserSession()
|
||||
|
||||
await prisma.wishLike.create({
|
||||
data: {
|
||||
wishId: wishId,
|
||||
userSession: userSession,
|
||||
}
|
||||
})
|
||||
|
||||
// 更新統計數據
|
||||
await StatisticsService.updateLikeStats(wishId, 'create')
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
// 如果是重複點讚錯誤,返回 false
|
||||
if (error.code === 'P2002') {
|
||||
return false
|
||||
}
|
||||
console.error("Error liking wish:", error)
|
||||
throw new MySQLServiceError("點讚失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消點讚
|
||||
static async unlikeWish(wishId: number): Promise<boolean> {
|
||||
try {
|
||||
const userSession = getUserSession()
|
||||
|
||||
const result = await prisma.wishLike.deleteMany({
|
||||
where: {
|
||||
wishId: wishId,
|
||||
userSession: userSession
|
||||
}
|
||||
})
|
||||
|
||||
if (result.count > 0) {
|
||||
// 更新統計數據
|
||||
await StatisticsService.updateLikeStats(wishId, 'delete')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error("Error unliking wish:", error)
|
||||
throw new MySQLServiceError("取消點讚失敗", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 檢查用戶是否已點讚
|
||||
static async hasUserLiked(wishId: number): Promise<boolean> {
|
||||
try {
|
||||
const userSession = getUserSession()
|
||||
|
||||
const like = await prisma.wishLike.findFirst({
|
||||
where: {
|
||||
wishId: wishId,
|
||||
userSession: userSession
|
||||
}
|
||||
})
|
||||
|
||||
return !!like
|
||||
} catch (error) {
|
||||
console.error("Error checking like status:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取困擾案例的點讚數
|
||||
static async getWishLikeCount(wishId: number): Promise<number> {
|
||||
try {
|
||||
const count = await prisma.wishLike.count({
|
||||
where: {
|
||||
wish_id: wishId
|
||||
}
|
||||
})
|
||||
|
||||
return count
|
||||
} catch (error) {
|
||||
console.error("Error fetching like count:", error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取用戶已點讚的困擾 ID 列表
|
||||
static async getUserLikedWishes(): Promise<number[]> {
|
||||
try {
|
||||
const userSession = getUserSession()
|
||||
|
||||
const likes = await prisma.wishLike.findMany({
|
||||
where: {
|
||||
userSession: userSession
|
||||
},
|
||||
select: {
|
||||
wishId: true
|
||||
}
|
||||
})
|
||||
|
||||
return likes.map((like: any) => like.wishId)
|
||||
} catch (error) {
|
||||
console.error("Error fetching user liked wishes:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用戶設定相關服務
|
||||
export class UserSettingsService {
|
||||
// 獲取用戶設定
|
||||
static async getUserSettings(): Promise<UserSettings | null> {
|
||||
try {
|
||||
const userSession = getUserSession()
|
||||
|
||||
const settings = await prisma.userSetting.findUnique({
|
||||
where: {
|
||||
userSession: userSession
|
||||
}
|
||||
})
|
||||
|
||||
return settings
|
||||
} catch (error) {
|
||||
console.error("Error fetching user settings:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 更新或創建用戶設定
|
||||
static async updateUserSettings(settings: {
|
||||
backgroundMusicEnabled?: boolean
|
||||
backgroundMusicVolume?: number
|
||||
backgroundMusicPlaying?: boolean
|
||||
themePreference?: string
|
||||
languagePreference?: string
|
||||
notificationEnabled?: boolean
|
||||
}): Promise<UserSettings> {
|
||||
try {
|
||||
const userSession = getUserSession()
|
||||
|
||||
const userSettings = await prisma.userSetting.upsert({
|
||||
where: {
|
||||
userSession: userSession
|
||||
},
|
||||
update: {
|
||||
backgroundMusicEnabled: settings.backgroundMusicEnabled,
|
||||
backgroundMusicVolume: settings.backgroundMusicVolume,
|
||||
backgroundMusicPlaying: settings.backgroundMusicPlaying,
|
||||
themePreference: settings.themePreference,
|
||||
languagePreference: settings.languagePreference,
|
||||
notificationEnabled: settings.notificationEnabled,
|
||||
},
|
||||
create: {
|
||||
userSession: userSession,
|
||||
backgroundMusicEnabled: settings.backgroundMusicEnabled ?? false,
|
||||
backgroundMusicVolume: settings.backgroundMusicVolume ?? 0.3,
|
||||
backgroundMusicPlaying: settings.backgroundMusicPlaying ?? false,
|
||||
themePreference: settings.themePreference ?? 'auto',
|
||||
languagePreference: settings.languagePreference ?? 'zh-TW',
|
||||
notificationEnabled: settings.notificationEnabled ?? true,
|
||||
}
|
||||
})
|
||||
|
||||
return userSettings
|
||||
} catch (error) {
|
||||
console.error("Error updating user settings:", error)
|
||||
throw new MySQLServiceError("更新用戶設定失敗", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 數據遷移服務(從 Supabase 遷移到 MySQL)
|
||||
export class MigrationService {
|
||||
// 遷移 Supabase 數據到 MySQL
|
||||
static async migrateFromSupabase(supabaseData: any[]): Promise<{
|
||||
success: number
|
||||
failed: number
|
||||
errors: string[]
|
||||
}> {
|
||||
const result = {
|
||||
success: 0,
|
||||
failed: 0,
|
||||
errors: [] as string[],
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Starting migration of ${supabaseData.length} wishes...`)
|
||||
|
||||
for (const wish of supabaseData) {
|
||||
try {
|
||||
await WishService.createWish({
|
||||
title: wish.title,
|
||||
currentPain: wish.current_pain,
|
||||
expectedSolution: wish.expected_solution,
|
||||
expectedEffect: wish.expected_effect,
|
||||
isPublic: wish.is_public,
|
||||
email: wish.email,
|
||||
images: wish.images || [],
|
||||
})
|
||||
result.success++
|
||||
} catch (error) {
|
||||
result.failed++
|
||||
result.errors.push(`Failed to migrate wish "${wish.title}": ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Migration completed: ${result.success} success, ${result.failed} failed`)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error("Migration error:", error)
|
||||
result.errors.push(`Migration process failed: ${error}`)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有數據(測試用)
|
||||
static async clearAllData(): Promise<void> {
|
||||
try {
|
||||
await prisma.wishLike.deleteMany()
|
||||
await prisma.wish.deleteMany()
|
||||
await prisma.userSetting.deleteMany()
|
||||
await prisma.migrationLog.deleteMany()
|
||||
await prisma.systemStat.deleteMany()
|
||||
console.log("All data cleared")
|
||||
} catch (error) {
|
||||
console.error("Error clearing data:", error)
|
||||
throw new MySQLServiceError("清空數據失敗", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 測試 MySQL 連接
|
||||
export async function testMySQLConnection(): Promise<boolean> {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`
|
||||
console.log("✅ MySQL connection successful")
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("MySQL connection test failed:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 關閉 Prisma 連接
|
||||
export async function closeMySQLConnection(): Promise<void> {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
|
||||
export default prisma
|
||||
268
lib/statistics-service.ts
Normal file
268
lib/statistics-service.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
// 統計服務 - 替代 MySQL 觸發器功能
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// 統計更新服務
|
||||
export class StatisticsService {
|
||||
// 更新困擾案例統計
|
||||
static async updateWishStats(wishId: number, action: 'create' | 'update' | 'delete', isPublic?: boolean) {
|
||||
try {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
if (action === 'create') {
|
||||
// 創建困擾案例
|
||||
await prisma.systemStat.upsert({
|
||||
where: { statDate: today },
|
||||
update: {
|
||||
totalWishes: { increment: 1 },
|
||||
publicWishes: { increment: isPublic ? 1 : 0 },
|
||||
privateWishes: { increment: isPublic ? 0 : 1 }
|
||||
},
|
||||
create: {
|
||||
statDate: today,
|
||||
totalWishes: 1,
|
||||
publicWishes: isPublic ? 1 : 0,
|
||||
privateWishes: isPublic ? 0 : 1,
|
||||
totalLikes: 0,
|
||||
activeUsers: 0,
|
||||
storageUsedMb: 0
|
||||
}
|
||||
})
|
||||
} else if (action === 'delete') {
|
||||
// 刪除困擾案例
|
||||
const existing = await prisma.systemStat.findUnique({
|
||||
where: { statDate: today }
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
await prisma.systemStat.update({
|
||||
where: { statDate: today },
|
||||
data: {
|
||||
totalWishes: Math.max(existing.totalWishes - 1, 0),
|
||||
publicWishes: Math.max(existing.publicWishes - (isPublic ? 1 : 0), 0),
|
||||
privateWishes: Math.max(existing.privateWishes - (isPublic ? 0 : 1), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (action === 'update') {
|
||||
// 更新困擾案例(公開狀態變更)
|
||||
const existing = await prisma.systemStat.findUnique({
|
||||
where: { statDate: today }
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
await prisma.systemStat.update({
|
||||
where: { statDate: today },
|
||||
data: {
|
||||
publicWishes: { increment: isPublic ? 1 : -1 },
|
||||
privateWishes: { increment: isPublic ? -1 : 1 }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新困擾案例統計失敗:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新點讚統計
|
||||
static async updateLikeStats(wishId: number, action: 'create' | 'delete') {
|
||||
try {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
if (action === 'create') {
|
||||
await prisma.systemStat.upsert({
|
||||
where: { statDate: today },
|
||||
update: {
|
||||
totalLikes: { increment: 1 }
|
||||
},
|
||||
create: {
|
||||
statDate: today,
|
||||
totalWishes: 0,
|
||||
publicWishes: 0,
|
||||
privateWishes: 0,
|
||||
totalLikes: 1,
|
||||
activeUsers: 0,
|
||||
storageUsedMb: 0
|
||||
}
|
||||
})
|
||||
} else if (action === 'delete') {
|
||||
const existing = await prisma.systemStat.findUnique({
|
||||
where: { statDate: today }
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
await prisma.systemStat.update({
|
||||
where: { statDate: today },
|
||||
data: {
|
||||
totalLikes: Math.max(existing.totalLikes - 1, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新點讚統計失敗:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新活躍用戶統計
|
||||
static async updateActiveUserStats(userSession: string) {
|
||||
try {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
// 檢查今天是否已經統計過這個用戶
|
||||
const existing = await prisma.systemStat.findUnique({
|
||||
where: { statDate: today }
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
// 簡單的活躍用戶計數(實際應用中可能需要更複雜的邏輯)
|
||||
await prisma.systemStat.update({
|
||||
where: { statDate: today },
|
||||
data: {
|
||||
activeUsers: { increment: 1 }
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await prisma.systemStat.create({
|
||||
data: {
|
||||
statDate: today,
|
||||
totalWishes: 0,
|
||||
publicWishes: 0,
|
||||
privateWishes: 0,
|
||||
totalLikes: 0,
|
||||
activeUsers: 1,
|
||||
storageUsedMb: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新活躍用戶統計失敗:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取統計數據
|
||||
static async getStatistics() {
|
||||
try {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
const stats = await prisma.systemStat.findUnique({
|
||||
where: { statDate: today }
|
||||
})
|
||||
|
||||
if (!stats) {
|
||||
return {
|
||||
totalWishes: 0,
|
||||
publicWishes: 0,
|
||||
privateWishes: 0,
|
||||
totalLikes: 0,
|
||||
activeUsers: 0,
|
||||
storageUsedMb: 0
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
} catch (error) {
|
||||
console.error('獲取統計數據失敗:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 重新計算所有統計數據
|
||||
static async recalculateAllStats() {
|
||||
try {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
// 計算總困擾數
|
||||
const totalWishes = await prisma.wish.count({
|
||||
where: { status: 'active' }
|
||||
})
|
||||
|
||||
// 計算公開困擾數
|
||||
const publicWishes = await prisma.wish.count({
|
||||
where: {
|
||||
status: 'active',
|
||||
isPublic: true
|
||||
}
|
||||
})
|
||||
|
||||
// 計算私密困擾數
|
||||
const privateWishes = await prisma.wish.count({
|
||||
where: {
|
||||
status: 'active',
|
||||
isPublic: false
|
||||
}
|
||||
})
|
||||
|
||||
// 計算總點讚數
|
||||
const totalLikes = await prisma.wishLike.count()
|
||||
|
||||
// 計算本週新增困擾數
|
||||
const thisWeek = new Date()
|
||||
thisWeek.setDate(thisWeek.getDate() - 7)
|
||||
const thisWeekWishes = await prisma.wish.count({
|
||||
where: {
|
||||
status: 'active',
|
||||
createdAt: { gte: thisWeek }
|
||||
}
|
||||
})
|
||||
|
||||
// 計算上週新增困擾數
|
||||
const lastWeekStart = new Date()
|
||||
lastWeekStart.setDate(lastWeekStart.getDate() - 14)
|
||||
const lastWeekEnd = new Date()
|
||||
lastWeekEnd.setDate(lastWeekEnd.getDate() - 7)
|
||||
const lastWeekWishes = await prisma.wish.count({
|
||||
where: {
|
||||
status: 'active',
|
||||
createdAt: {
|
||||
gte: lastWeekStart,
|
||||
lt: lastWeekEnd
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 更新或創建統計記錄
|
||||
await prisma.systemStat.upsert({
|
||||
where: { statDate: today },
|
||||
update: {
|
||||
totalWishes,
|
||||
publicWishes,
|
||||
privateWishes,
|
||||
totalLikes,
|
||||
activeUsers: 0, // 需要更複雜的邏輯來計算活躍用戶
|
||||
storageUsedMb: 0 // 需要計算實際儲存使用量
|
||||
},
|
||||
create: {
|
||||
statDate: today,
|
||||
totalWishes,
|
||||
publicWishes,
|
||||
privateWishes,
|
||||
totalLikes,
|
||||
activeUsers: 0,
|
||||
storageUsedMb: 0
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
totalWishes,
|
||||
publicWishes,
|
||||
privateWishes,
|
||||
totalLikes,
|
||||
thisWeekWishes,
|
||||
lastWeekWishes
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新計算統計數據失敗:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StatisticsService
|
||||
@@ -26,6 +26,10 @@ export class WishService {
|
||||
// 獲取所有公開的困擾案例(帶點讚數)
|
||||
static async getPublicWishes(): Promise<Wish[]> {
|
||||
try {
|
||||
if (!supabase) {
|
||||
throw new SupabaseError("Supabase 未配置,請使用統一的資料庫服務")
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("wishes_with_likes")
|
||||
.select("*")
|
||||
@@ -43,6 +47,10 @@ export class WishService {
|
||||
// 獲取所有困擾案例(用於分析,包含私密的)
|
||||
static async getAllWishes(): Promise<Wish[]> {
|
||||
try {
|
||||
if (!supabase) {
|
||||
throw new SupabaseError("Supabase 未配置,請使用統一的資料庫服務")
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("wishes_with_likes")
|
||||
.select("*")
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
|
||||
// Supabase 配置
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
|
||||
// 創建 Supabase 客戶端(單例模式)
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
// 只有在有 Supabase 環境變數時才創建客戶端
|
||||
export const supabase = supabaseUrl && supabaseAnonKey ? createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
persistSession: false, // 我們不需要用戶認證
|
||||
},
|
||||
db: {
|
||||
schema: "public",
|
||||
},
|
||||
})
|
||||
}) : null
|
||||
|
||||
// 數據庫類型定義
|
||||
export interface Database {
|
||||
@@ -136,6 +137,11 @@ export function getUserSession(): string {
|
||||
// 測試 Supabase 連接
|
||||
export async function testSupabaseConnection(): Promise<boolean> {
|
||||
try {
|
||||
if (!supabase) {
|
||||
console.log("ℹ️ Supabase 未配置,使用 MySQL 資料庫")
|
||||
return false
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.from("wishes").select("count").limit(1)
|
||||
|
||||
if (error) {
|
||||
|
||||
Reference in New Issue
Block a user