add annual

This commit is contained in:
songtianlun 2025-06-14 02:44:53 +08:00
parent 42b3c0950e
commit 65014ef907
4 changed files with 484 additions and 73 deletions

View File

@ -1,6 +1,170 @@
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@/auth"
import { db } from "@/lib/db"
import { z } from "zod"
const updateProductSchema = z.object({
principal: z.number().positive("本金必须大于0"),
depositDate: z.string().nullable().optional(),
endDate: z.string().nullable().optional(),
currentNetValue: z.number().nullable().optional(),
annualRate: z.number().nullable().optional(),
}).refine((data) => {
// 如果有开始时间、结束时间、本金、当前净值,则不需要年化利率(会自动计算)
// 否则至少需要当前净值或年化利率中的一个
const hasCompleteInfo = data.depositDate && data.endDate && data.principal && data.currentNetValue
const hasEitherValue = data.currentNetValue !== null || data.annualRate !== null
return hasCompleteInfo || hasEitherValue
}, {
message: "请提供足够的信息:要么提供当前净值或年化利率,要么提供完整的时间和净值信息",
path: ["currentNetValue", "annualRate"]
})
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
// 检查产品是否存在且属于当前用户
const existingProduct = await db.financeProduct.findUnique({
where: {
id: params.id,
},
})
if (!existingProduct) {
return NextResponse.json({ error: "Product not found" }, { status: 404 })
}
if (existingProduct.userId !== session.user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
}
const body = await request.json()
console.log("Received update request body:", body)
// 预处理数据将空字符串转换为null
const processedBody = {
...body,
currentNetValue: body.currentNetValue === "" || body.currentNetValue === null ? null : Number(body.currentNetValue),
annualRate: body.annualRate === "" || body.annualRate === null ? null : Number(body.annualRate),
principal: Number(body.principal),
depositDate: body.depositDate || null,
endDate: body.endDate || null,
}
console.log("Processed update request body:", processedBody)
const validatedData = updateProductSchema.parse(processedBody)
console.log("Validated update data:", validatedData)
// 计算相关字段
let calculatedValues = {
profit: null as number | null,
dailyProfit: null as number | null,
monthlyProfit: null as number | null,
calculatedAnnualRate: null as number | null,
averageAnnualProfit: null as number | null,
}
// 验证日期
let depositDate: Date | null = null
let endDate: Date | null = null
let days = 0
if (validatedData.depositDate && validatedData.endDate) {
depositDate = new Date(validatedData.depositDate)
endDate = new Date(validatedData.endDate)
if (isNaN(depositDate.getTime()) || isNaN(endDate.getTime())) {
return NextResponse.json(
{ error: "无效的日期格式" },
{ status: 400 }
)
}
days = Math.max(1, Math.floor((endDate.getTime() - depositDate.getTime()) / (1000 * 60 * 60 * 24)))
}
// 如果有当前净值,计算收益
if (validatedData.currentNetValue !== null && validatedData.currentNetValue !== undefined) {
calculatedValues.profit = validatedData.currentNetValue - validatedData.principal
}
// 自动计算年化利率(如果没有提供年化利率但有完整信息)
if (!validatedData.annualRate && validatedData.currentNetValue && depositDate && endDate && days > 0) {
const totalReturn = validatedData.currentNetValue - validatedData.principal
const dailyReturn = totalReturn / days
const annualReturn = (dailyReturn * 365) / validatedData.principal
validatedData.annualRate = annualReturn * 100 // 转换为百分比
console.log("Auto-calculated annual rate:", validatedData.annualRate)
}
// 如果有年化利率和日期,计算当前净值(当没有提供当前净值时)
if (validatedData.annualRate && depositDate && endDate && days > 0 && !validatedData.currentNetValue) {
const calculatedNetValue = validatedData.principal * (1 + ((validatedData.annualRate / 100) * days) / 365)
validatedData.currentNetValue = calculatedNetValue
calculatedValues.profit = calculatedNetValue - validatedData.principal
console.log("Auto-calculated net value:", validatedData.currentNetValue)
}
// 计算日收益和月收益
if (calculatedValues.profit !== null && days > 0) {
calculatedValues.dailyProfit = calculatedValues.profit / days
calculatedValues.monthlyProfit = calculatedValues.dailyProfit * 30
calculatedValues.calculatedAnnualRate = ((calculatedValues.dailyProfit * 365) / validatedData.principal) * 100
calculatedValues.averageAnnualProfit = calculatedValues.dailyProfit * 365
}
console.log("Calculated values for update:", calculatedValues)
const updatedProduct = await db.financeProduct.update({
where: {
id: params.id,
},
data: {
principal: validatedData.principal,
depositDate: depositDate,
endDate: endDate,
currentNetValue: validatedData.currentNetValue || null,
annualRate: validatedData.annualRate || null,
...calculatedValues,
},
})
console.log("Updated product:", updatedProduct)
return NextResponse.json(updatedProduct)
} catch (error) {
console.error("Error in PUT /api/products/[id]:", error)
if (error instanceof z.ZodError) {
console.error("Zod validation errors:", error.errors)
return NextResponse.json(
{
error: "数据验证失败",
details: error.errors.map(err => ({
field: err.path.join('.'),
message: err.message
}))
},
{ status: 400 }
)
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,

View File

@ -10,10 +10,14 @@ const createProductSchema = z.object({
currentNetValue: z.number().nullable().optional(),
annualRate: z.number().nullable().optional(),
}).refine((data) => {
// 至少有一个:当前净值或年化利率
return data.currentNetValue !== null || data.annualRate !== null
// 如果有开始时间、结束时间、本金、当前净值,则不需要年化利率(会自动计算)
// 否则至少需要当前净值或年化利率中的一个
const hasCompleteInfo = data.depositDate && data.endDate && data.principal && data.currentNetValue
const hasEitherValue = data.currentNetValue !== null || data.annualRate !== null
return hasCompleteInfo || hasEitherValue
}, {
message: "当前净值和年化利率不能同时为空",
message: "请提供足够的信息:要么提供当前净值或年化利率,要么提供完整的时间和净值信息",
path: ["currentNetValue", "annualRate"]
})
@ -76,19 +80,18 @@ export async function POST(request: NextRequest) {
dailyProfit: null as number | null,
monthlyProfit: null as number | null,
calculatedAnnualRate: null as number | null,
averageAnnualProfit: null as number | null,
}
// 如果有当前净值,计算收益
if (validatedData.currentNetValue !== null && validatedData.currentNetValue !== undefined) {
calculatedValues.profit = validatedData.currentNetValue - validatedData.principal
}
// 验证日期
let depositDate: Date | null = null
let endDate: Date | null = null
let days = 0
// 如果有年化利率和日期,计算当前净值
if (validatedData.annualRate && validatedData.depositDate && validatedData.endDate) {
const depositDate = new Date(validatedData.depositDate)
const endDate = new Date(validatedData.endDate)
if (validatedData.depositDate && validatedData.endDate) {
depositDate = new Date(validatedData.depositDate)
endDate = new Date(validatedData.endDate)
// 验证日期是否有效
if (isNaN(depositDate.getTime()) || isNaN(endDate.getTime())) {
return NextResponse.json(
{ error: "无效的日期格式" },
@ -96,24 +99,37 @@ export async function POST(request: NextRequest) {
)
}
const days = Math.max(1, Math.floor((endDate.getTime() - depositDate.getTime()) / (1000 * 60 * 60 * 24)))
if (!validatedData.currentNetValue) {
const calculatedNetValue = validatedData.principal * (1 + ((validatedData.annualRate / 100) * days) / 365)
validatedData.currentNetValue = calculatedNetValue
calculatedValues.profit = calculatedNetValue - validatedData.principal
}
days = Math.max(1, Math.floor((endDate.getTime() - depositDate.getTime()) / (1000 * 60 * 60 * 24)))
}
// 如果有当前净值,计算收益
if (validatedData.currentNetValue !== null && validatedData.currentNetValue !== undefined) {
calculatedValues.profit = validatedData.currentNetValue - validatedData.principal
}
// 自动计算年化利率(如果没有提供年化利率但有完整信息)
if (!validatedData.annualRate && validatedData.currentNetValue && depositDate && endDate && days > 0) {
const totalReturn = validatedData.currentNetValue - validatedData.principal
const dailyReturn = totalReturn / days
const annualReturn = (dailyReturn * 365) / validatedData.principal
validatedData.annualRate = annualReturn * 100 // 转换为百分比
console.log("Auto-calculated annual rate:", validatedData.annualRate)
}
// 如果有年化利率和日期,计算当前净值(当没有提供当前净值时)
if (validatedData.annualRate && depositDate && endDate && days > 0 && !validatedData.currentNetValue) {
const calculatedNetValue = validatedData.principal * (1 + ((validatedData.annualRate / 100) * days) / 365)
validatedData.currentNetValue = calculatedNetValue
calculatedValues.profit = calculatedNetValue - validatedData.principal
console.log("Auto-calculated net value:", validatedData.currentNetValue)
}
// 计算日收益和月收益
if (calculatedValues.profit !== null && validatedData.depositDate && validatedData.endDate) {
const depositDate = new Date(validatedData.depositDate)
const endDate = new Date(validatedData.endDate)
const days = Math.max(1, Math.floor((endDate.getTime() - depositDate.getTime()) / (1000 * 60 * 60 * 24)))
if (calculatedValues.profit !== null && days > 0) {
calculatedValues.dailyProfit = calculatedValues.profit / days
calculatedValues.monthlyProfit = calculatedValues.dailyProfit * 30
calculatedValues.calculatedAnnualRate = ((calculatedValues.dailyProfit * 365) / validatedData.principal) * 100
calculatedValues.averageAnnualProfit = calculatedValues.dailyProfit * 365
}
console.log("Calculated values:", calculatedValues)
@ -122,8 +138,8 @@ export async function POST(request: NextRequest) {
data: {
userId: session.user.id,
principal: validatedData.principal,
depositDate: validatedData.depositDate ? new Date(validatedData.depositDate) : null,
endDate: validatedData.endDate ? new Date(validatedData.endDate) : null,
depositDate: depositDate,
endDate: endDate,
currentNetValue: validatedData.currentNetValue || null,
annualRate: validatedData.annualRate || null,
...calculatedValues,

View File

@ -7,7 +7,7 @@ import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Label } from "@/components/ui/label"
import { CalendarIcon, PlusCircle, Trash2, LogIn } from "lucide-react"
import { CalendarIcon, PlusCircle, Trash2, LogIn, Edit, Check, X } from "lucide-react"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { cn } from "@/lib/utils"
@ -27,6 +27,7 @@ interface FinanceProduct {
dailyProfit: number | null
monthlyProfit: number | null
calculatedAnnualRate: number | null
averageAnnualProfit: number | null
createdAt?: Date
updatedAt?: Date
}
@ -46,6 +47,11 @@ export default function FinanceCalculator() {
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
// 编辑相关状态
const [editingProductId, setEditingProductId] = useState<string | null>(null)
const [editingProduct, setEditingProduct] = useState<NewProduct>(createEmptyProduct())
const [isUpdating, setIsUpdating] = useState(false)
function createEmptyProduct(): NewProduct {
const lastProduct = products.length > 0 ? products[products.length - 1] : null
@ -106,9 +112,12 @@ export default function FinanceCalculator() {
return
}
// 验证输入
if (newProduct.currentNetValue === null && newProduct.annualRate === null) {
setError("当前净值和年化利率不能同时为空")
// 更新验证逻辑:如果有完整的时间和净值信息,则不需要年化利率
const hasCompleteInfo = newProduct.depositDate && newProduct.endDate && newProduct.principal && newProduct.currentNetValue
const hasEitherValue = newProduct.currentNetValue !== null || newProduct.annualRate !== null
if (!hasCompleteInfo && !hasEitherValue) {
setError("请提供足够的信息:要么提供当前净值或年化利率,要么提供完整的时间和净值信息以自动计算年化利率")
return
}
@ -172,6 +181,101 @@ export default function FinanceCalculator() {
}
}
// 开始编辑产品
function handleEditProduct(product: FinanceProduct) {
setEditingProductId(product.id)
setEditingProduct({
principal: product.principal,
depositDate: product.depositDate,
endDate: product.endDate,
currentNetValue: product.currentNetValue,
annualRate: product.annualRate,
})
}
// 取消编辑
function handleCancelEdit() {
setEditingProductId(null)
setEditingProduct(createEmptyProduct())
}
// 保存编辑
async function handleSaveEdit() {
if (!session?.user?.id || !editingProductId) {
toast.error('请先登录')
return
}
if (editingProduct.principal <= 0) {
toast.error("本金必须大于0")
return
}
setIsUpdating(true)
try {
const requestData = {
principal: editingProduct.principal,
depositDate: editingProduct.depositDate?.toISOString() || null,
endDate: editingProduct.endDate?.toISOString() || null,
currentNetValue: editingProduct.currentNetValue,
annualRate: editingProduct.annualRate,
}
console.log('Updating product with data:', requestData)
const response = await fetch(`/api/products/${editingProductId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
})
const responseData = await response.json()
console.log('Update API response:', responseData)
if (response.ok) {
// 转换日期字符串为Date对象
const updatedProductWithDates = {
...responseData,
depositDate: responseData.depositDate ? new Date(responseData.depositDate) : null,
endDate: responseData.endDate ? new Date(responseData.endDate) : null,
createdAt: responseData.createdAt ? new Date(responseData.createdAt) : undefined,
updatedAt: responseData.updatedAt ? new Date(responseData.updatedAt) : undefined,
}
// 更新产品列表
setProducts(products.map(p =>
p.id === editingProductId ? updatedProductWithDates : p
))
// 清除编辑状态
setEditingProductId(null)
setEditingProduct(createEmptyProduct())
toast.success('产品更新成功')
} else {
console.error('Update API error response:', responseData)
if (responseData.details && Array.isArray(responseData.details)) {
// 显示详细的验证错误
const errorMessages = responseData.details.map((detail: any) =>
`${detail.field}: ${detail.message}`
).join(', ')
toast.error(`数据验证失败: ${errorMessages}`)
} else {
toast.error(responseData.error || '更新产品失败')
}
}
} catch (error) {
console.error('Update request error:', error)
toast.error('网络请求失败,请重试')
} finally {
setIsUpdating(false)
}
}
async function handleRemoveProduct(id: string) {
if (!session?.user?.id) {
toast.error('请先登录')
@ -201,6 +305,13 @@ export default function FinanceCalculator() {
})
}
function handleUseCurrentTimeForEdit() {
setEditingProduct({
...editingProduct,
endDate: new Date(),
})
}
// 当产品列表变化时,更新新产品的默认本金
useEffect(() => {
if (products.length > 0) {
@ -348,7 +459,7 @@ export default function FinanceCalculator() {
annualRate: e.target.value ? Number.parseFloat(e.target.value) : null,
})
}
placeholder="输入年化利率"
placeholder="输入年化利率(可自动计算)"
/>
</div>
@ -385,8 +496,8 @@ export default function FinanceCalculator() {
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> (%)</TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[120px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -399,46 +510,165 @@ export default function FinanceCalculator() {
) : (
products.map((product) => (
<TableRow key={product.id}>
<TableCell>{product.principal.toFixed(2)}</TableCell>
<TableCell>
{product.depositDate ? format(product.depositDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
</TableCell>
<TableCell>
{product.endDate ? format(product.endDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
</TableCell>
<TableCell>
{product.currentNetValue !== null ? product.currentNetValue.toFixed(2) : "-"}
</TableCell>
<TableCell>{product.annualRate !== null ? product.annualRate.toFixed(2) : "-"}</TableCell>
<TableCell
className={cn(
product.profit && product.profit > 0
? "text-green-600"
: product.profit && product.profit < 0
? "text-red-600"
: "",
)}
>
{product.profit !== null ? product.profit.toFixed(2) : "-"}
</TableCell>
<TableCell>{product.dailyProfit !== null ? product.dailyProfit.toFixed(2) : "-"}</TableCell>
<TableCell>{product.monthlyProfit !== null ? product.monthlyProfit.toFixed(2) : "-"}</TableCell>
<TableCell
className={cn(
product.calculatedAnnualRate && product.calculatedAnnualRate > 0
? "text-green-600"
: product.calculatedAnnualRate && product.calculatedAnnualRate < 0
? "text-red-600"
: "",
)}
>
{product.calculatedAnnualRate !== null ? product.calculatedAnnualRate.toFixed(2) : "-"}
</TableCell>
<TableCell>
<Button variant="ghost" size="icon" onClick={() => handleRemoveProduct(product.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
{editingProductId === product.id ? (
// 编辑模式的行
<>
<TableCell>
<Input
type="number"
value={editingProduct.principal || ""}
onChange={(e) => setEditingProduct({
...editingProduct,
principal: Number.parseFloat(e.target.value) || 0
})}
className="w-20"
/>
</TableCell>
<TableCell>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<CalendarIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={editingProduct.depositDate || undefined}
onSelect={(date) => setEditingProduct({
...editingProduct,
depositDate: date || null
})}
initialFocus
/>
</PopoverContent>
</Popover>
</TableCell>
<TableCell>
<div className="flex gap-1">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<CalendarIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={editingProduct.endDate || undefined}
onSelect={(date) => setEditingProduct({
...editingProduct,
endDate: date || null
})}
initialFocus
/>
</PopoverContent>
</Popover>
<Button
variant="ghost"
size="sm"
onClick={handleUseCurrentTimeForEdit}
>
</Button>
</div>
</TableCell>
<TableCell>
<Input
type="number"
value={editingProduct.currentNetValue !== null ? editingProduct.currentNetValue : ""}
onChange={(e) => setEditingProduct({
...editingProduct,
currentNetValue: e.target.value ? Number.parseFloat(e.target.value) : null
})}
className="w-24"
/>
</TableCell>
<TableCell>
<Input
type="number"
value={editingProduct.annualRate !== null ? editingProduct.annualRate : ""}
onChange={(e) => setEditingProduct({
...editingProduct,
annualRate: e.target.value ? Number.parseFloat(e.target.value) : null
})}
className="w-20"
placeholder="自动计算"
/>
</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
onClick={handleSaveEdit}
disabled={isUpdating}
>
<Check className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleCancelEdit}
disabled={isUpdating}
>
<X className="h-4 w-4" />
</Button>
</div>
</TableCell>
</>
) : (
// 普通显示模式的行
<>
<TableCell>{product.principal.toFixed(2)}</TableCell>
<TableCell>
{product.depositDate ? format(product.depositDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
</TableCell>
<TableCell>
{product.endDate ? format(product.endDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
</TableCell>
<TableCell>
{product.currentNetValue !== null ? product.currentNetValue.toFixed(2) : "-"}
</TableCell>
<TableCell>{product.annualRate !== null ? product.annualRate.toFixed(2) : "-"}</TableCell>
<TableCell
className={cn(
product.profit && product.profit > 0
? "text-green-600"
: product.profit && product.profit < 0
? "text-red-600"
: "",
)}
>
{product.profit !== null ? product.profit.toFixed(2) : "-"}
</TableCell>
<TableCell>{product.dailyProfit !== null ? product.dailyProfit.toFixed(2) : "-"}</TableCell>
<TableCell>{product.monthlyProfit !== null ? product.monthlyProfit.toFixed(2) : "-"}</TableCell>
<TableCell>{product.averageAnnualProfit !== null ? product.averageAnnualProfit.toFixed(2) : "-"}</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditProduct(product)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveProduct(product.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</>
)}
</TableRow>
))
)}

View File

@ -70,6 +70,7 @@ model FinanceProduct {
dailyProfit Float?
monthlyProfit Float?
calculatedAnnualRate Float?
averageAnnualProfit Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt