|
@@ -0,0 +1,192 @@
|
|
|
|
|
+import { Repository, Between, Like } from 'typeorm'
|
|
|
|
|
+import { FastifyInstance } from 'fastify'
|
|
|
|
|
+import { Finance, FinanceStatus } from '../entities/finance.entity'
|
|
|
|
|
+import { PaginationResponse } from '../dto/common.dto'
|
|
|
|
|
+import { CreateFinanceBody, UpdateFinanceBody, ListFinanceQuery } from '../dto/finance.dto'
|
|
|
|
|
+import { FileService } from './file.service'
|
|
|
|
|
+
|
|
|
|
|
+export class FinanceService {
|
|
|
|
|
+ private financeRepository: Repository<Finance>
|
|
|
|
|
+ private fileService: FileService
|
|
|
|
|
+
|
|
|
|
|
+ constructor(app: FastifyInstance) {
|
|
|
|
|
+ this.financeRepository = app.dataSource.getRepository(Finance)
|
|
|
|
|
+ this.fileService = new FileService(app)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async create(data: CreateFinanceBody): Promise<Finance> {
|
|
|
|
|
+ const finance = this.financeRepository.create(data)
|
|
|
|
|
+ return this.financeRepository.save(finance)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async findById(id: number): Promise<Finance> {
|
|
|
|
|
+ const finance = await this.financeRepository.findOneOrFail({ where: { id } })
|
|
|
|
|
+ // 处理支付二维码链接
|
|
|
|
|
+ finance.paymentQrCode = await this.processImageUrl(finance.paymentQrCode)
|
|
|
|
|
+ return finance
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理图片链接,如果是OSS链接则生成签名URL
|
|
|
|
|
+ * @param imageUrl 原始图片URL
|
|
|
|
|
+ * @returns 处理后的图片URL
|
|
|
|
|
+ */
|
|
|
|
|
+ private async processImageUrl(imageUrl: string): Promise<string> {
|
|
|
|
|
+ if (!imageUrl) return imageUrl
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 检查是否为OSS链接
|
|
|
|
|
+ if (imageUrl.includes('.oss-') && imageUrl.includes('.aliyuncs.com')) {
|
|
|
|
|
+ // 从URL中提取key
|
|
|
|
|
+ const urlParts = imageUrl.split('.aliyuncs.com/')
|
|
|
|
|
+ if (urlParts.length > 1) {
|
|
|
|
|
+ const key = urlParts[1]
|
|
|
|
|
+ // 生成签名URL
|
|
|
|
|
+ return await this.fileService.getSignedUrl(key)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return imageUrl
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ // 如果签名失败,返回原始URL
|
|
|
|
|
+ console.warn('图片链接签名失败:', error)
|
|
|
|
|
+ return imageUrl
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量处理图片链接
|
|
|
|
|
+ * @param finances 财务记录数组
|
|
|
|
|
+ * @returns 处理后的财务记录数组
|
|
|
|
|
+ */
|
|
|
|
|
+ private async processImageUrls(finances: Finance[]): Promise<Finance[]> {
|
|
|
|
|
+ const processedFinances = await Promise.all(
|
|
|
|
|
+ finances.map(async (finance) => {
|
|
|
|
|
+ const processedPaymentQrCode = await this.processImageUrl(finance.paymentQrCode)
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...finance,
|
|
|
|
|
+ paymentQrCode: processedPaymentQrCode
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ return processedFinances
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async findAll(query: ListFinanceQuery): Promise<PaginationResponse<Finance>> {
|
|
|
|
|
+ const { page, size, status, paymentName, bankName, startDate, endDate } = query
|
|
|
|
|
+
|
|
|
|
|
+ const where: any = {}
|
|
|
|
|
+
|
|
|
|
|
+ if (status) {
|
|
|
|
|
+ where.status = status
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (paymentName) {
|
|
|
|
|
+ where.paymentName = Like(`%${paymentName}%`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (bankName) {
|
|
|
|
|
+ where.bankName = Like(`%${bankName}%`)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (startDate && endDate) {
|
|
|
|
|
+ where.createdAt = Between(new Date(startDate), new Date(endDate))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const [records, total] = await this.financeRepository.findAndCount({
|
|
|
|
|
+ where,
|
|
|
|
|
+ skip: (Number(page) || 0) * (Number(size) || 20),
|
|
|
|
|
+ take: Number(size) || 20,
|
|
|
|
|
+ order: { createdAt: 'DESC' }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 处理支付二维码链接,生成签名URL
|
|
|
|
|
+ const processedRecords = await this.processImageUrls(records)
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: processedRecords,
|
|
|
|
|
+ metadata: {
|
|
|
|
|
+ total: Number(total),
|
|
|
|
|
+ page: Number(page) || 0,
|
|
|
|
|
+ size: Number(size) || 20
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async update(data: UpdateFinanceBody): Promise<Finance> {
|
|
|
|
|
+ const { id, ...updateData } = data
|
|
|
|
|
+ await this.financeRepository.update(id, updateData)
|
|
|
|
|
+ return this.findById(id)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async delete(id: number): Promise<void> {
|
|
|
|
|
+ await this.financeRepository.update(id, { delFlag: true })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async hardDelete(id: number): Promise<void> {
|
|
|
|
|
+ await this.financeRepository.delete(id)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async updateStatus(id: number, status: FinanceStatus, rejectReason?: string): Promise<Finance> {
|
|
|
|
|
+ const updateData: any = { status }
|
|
|
|
|
+
|
|
|
|
|
+ if (status === FinanceStatus.WITHDRAWN || status === FinanceStatus.REJECTED) {
|
|
|
|
|
+ updateData.processedAt = new Date()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (status === FinanceStatus.REJECTED && rejectReason) {
|
|
|
|
|
+ updateData.rejectReason = rejectReason
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await this.financeRepository.update(id, updateData)
|
|
|
|
|
+ return this.findById(id)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async getStatistics(startDate?: string, endDate?: string): Promise<{
|
|
|
|
|
+ totalAmount: number
|
|
|
|
|
+ totalCount: number
|
|
|
|
|
+ byStatus: Record<FinanceStatus, { amount: number; count: number }>
|
|
|
|
|
+ pendingAmount: number
|
|
|
|
|
+ processedAmount: number
|
|
|
|
|
+ }> {
|
|
|
|
|
+ const where: any = {}
|
|
|
|
|
+
|
|
|
|
|
+ if (startDate && endDate) {
|
|
|
|
|
+ where.createdAt = Between(new Date(startDate), new Date(endDate))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const records = await this.financeRepository.find({
|
|
|
|
|
+ where,
|
|
|
|
|
+ select: ['reminderAmount', 'status']
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const statistics = {
|
|
|
|
|
+ totalAmount: 0,
|
|
|
|
|
+ totalCount: records.length,
|
|
|
|
|
+ byStatus: {} as Record<FinanceStatus, { amount: number; count: number }>,
|
|
|
|
|
+ pendingAmount: 0,
|
|
|
|
|
+ processedAmount: 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化统计对象
|
|
|
|
|
+ Object.values(FinanceStatus).forEach(status => {
|
|
|
|
|
+ statistics.byStatus[status] = { amount: 0, count: 0 }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 计算统计数据
|
|
|
|
|
+ records.forEach(record => {
|
|
|
|
|
+ const amount = Number(record.reminderAmount)
|
|
|
|
|
+ statistics.totalAmount += amount
|
|
|
|
|
+
|
|
|
|
|
+ statistics.byStatus[record.status].amount += amount
|
|
|
|
|
+ statistics.byStatus[record.status].count += 1
|
|
|
|
|
+
|
|
|
|
|
+ if (record.status === FinanceStatus.PROCESSING) {
|
|
|
|
|
+ statistics.pendingAmount += amount
|
|
|
|
|
+ } else {
|
|
|
|
|
+ statistics.processedAmount += amount
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return statistics
|
|
|
|
|
+ }
|
|
|
|
|
+}
|