+ google drive

This commit is contained in:
xds
2026-03-10 15:28:19 +03:00
parent 35852ae0c9
commit b676cb28a9
9 changed files with 140 additions and 5 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -8,6 +8,7 @@ data class SpaceDTO(
val name: String,
val owner: UserDTO,
val participants: Set<UserDTO> = emptySet(),
val isGoogleDriveConnected: Boolean = false,
val createdBy: UserDTO? = null,
val createdAt: Instant,
var updatedBy: UserDTO? = null,

View File

@@ -9,7 +9,8 @@ data class UserDTO(
var tgId: Long? = null,
var tgUserName: String? = null,
var photoUrl: String? = null,
var roles: List<String>
var roles: List<String>,
var isGoogleDriveConnected: Boolean = false
) {

View File

@@ -10,6 +10,7 @@ object SpaceMapper {
name = this.name,
owner = this.owner.toDto(),
participants = this.participants.map { it.toDto() }.toSet(),
isGoogleDriveConnected = this.owner.googleRefreshToken != null,
createdBy = this.createdBy?.toDto(),
createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"),
updatedBy = this.updatedBy?.toDto(),

View File

@@ -13,7 +13,8 @@ object UserMapper {
tgId = this.tgId,
tgUserName = this.tgUserName,
photoUrl = this.photoUrl,
roles = this.roles
roles = this.roles,
isGoogleDriveConnected = this.googleRefreshToken != null
)
fun TelegramAuthDTO.toTelegramMap(): Map<String, String> =

View File

@@ -42,6 +42,7 @@ class SpaceRepoImpl(
rs.getString("s_owner_username"),
rs.getString("s_owner_firstname"),
tgId = rs.getLong("s_owner_tg_id"),
googleRefreshToken = rs.getString("s_owner_google_refresh_token")
),
participant = User(rs.getInt("sp_uid"), rs.getString("sp_username"), rs.getString("sp_first_name")),
createdAt = rs.getTimestamp("s_created_at").toInstant(),
@@ -111,6 +112,7 @@ class SpaceRepoImpl(
ou.username as s_owner_username,
ou.first_name as s_owner_firstname,
ou.tg_id as s_owner_tg_id,
ou.google_refresh_token as s_owner_google_refresh_token,
sp.participants_id as sp_uid,
u.username as sp_username,
u.first_name as sp_first_name,
@@ -131,7 +133,7 @@ class SpaceRepoImpl(
left join finance.users uau on s.updated_by_id = uau.id
left join finance.transactions t on t.space_id = s.id
where s.is_deleted = false and t.created_at >= :lastRun
group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
group by s.id, ou.username, ou.first_name, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
uau.username, uau.first_name;
""".trimMargin()
val params = mapOf("lastRun" to lastRun)
@@ -150,6 +152,7 @@ class SpaceRepoImpl(
ou.username as s_owner_username,
ou.first_name as s_owner_firstname,
ou.tg_id as s_owner_tg_id,
ou.google_refresh_token as s_owner_google_refresh_token,
sp.participants_id as sp_uid,
u.username as sp_username,
u.first_name as sp_first_name,
@@ -171,7 +174,7 @@ class SpaceRepoImpl(
where (s.owner_id = :user_id
or sp.participants_id = :user_id)
and s.is_deleted = false
group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
group by s.id, ou.username, ou.first_name, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
uau.username, uau.first_name;
""".trimMargin()
val params = mapOf(
@@ -191,6 +194,7 @@ class SpaceRepoImpl(
ou.username as s_owner_username,
ou.first_name as s_owner_firstname,
ou.tg_id as s_owner_tg_id,
ou.google_refresh_token as s_owner_google_refresh_token,
sp.participants_id as sp_uid,
u.username as sp_username,
u.first_name as sp_first_name,
@@ -212,7 +216,7 @@ from finance.spaces s
where (s.owner_id = :user_id
or sp.participants_id = :user_id)
and s.is_deleted = false and s.id = :spaceId
group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
group by s.id, ou.username, ou.first_name, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
uau.username, uau.first_name;
""".trimMargin()
val params = mapOf(

View File

@@ -0,0 +1,68 @@
package space.luminic.finance.services
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow
import com.google.api.client.auth.oauth2.BearerToken
import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.http.ByteArrayContent
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.drive.Drive
import com.google.api.services.drive.DriveScopes
import com.google.api.services.drive.model.File
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
@Service
class GoogleDriveService(
@Value("\${google.client-id:}") private val clientId: String,
@Value("\${google.client-secret:}") private val clientSecret: String,
@Value("\${google.redirect-uri:}") private val redirectUri: String
) {
private val jsonFactory = GsonFactory.getDefaultInstance()
private val httpTransport = GoogleNetHttpTransport.newTrustedTransport()
fun exchangeCodeForRefreshToken(authCode: String): String {
if (clientId.isEmpty() || clientSecret.isEmpty()) return ""
val flow = GoogleAuthorizationCodeFlow.Builder(
httpTransport,
jsonFactory,
clientId,
clientSecret,
listOf(DriveScopes.DRIVE_FILE)
)
.setAccessType("offline")
.build()
val response = flow.newTokenRequest(authCode).setRedirectUri(redirectUri).execute()
return response.refreshToken ?: ""
}
fun uploadExcelFile(refreshToken: String, fileName: String, content: ByteArray) {
if (refreshToken.isEmpty() || clientId.isEmpty() || clientSecret.isEmpty()) return
val credential = GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setClientSecrets(clientId, clientSecret)
.build()
.setRefreshToken(refreshToken)
val driveService = Drive.Builder(httpTransport, jsonFactory, credential)
.setApplicationName("Luminic Space")
.build()
val fileMetadata = File()
fileMetadata.name = fileName
fileMetadata.mimeType = "application/vnd.google-apps.spreadsheet"
val mediaContent = ByteArrayContent("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", content)
driveService.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute()
}
}

View File

@@ -0,0 +1,50 @@
package space.luminic.finance.services
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import space.luminic.finance.repos.SpaceRepo
import java.time.LocalDate
@Service
class MonthlyExportScheduler(
private val spaceRepo: SpaceRepo,
private val transactionService: TransactionService,
private val googleDriveService: GoogleDriveService
) {
private val logger = LoggerFactory.getLogger(javaClass)
// Каждое 9 число в 23:59
@Scheduled(cron = "0 59 23 9 * ?")
fun exportMonthlyTransactions() {
logger.info("Starting monthly transaction export to Google Drive")
val spaces = spaceRepo.findAll()
val endDate = LocalDate.now() // 9th of current month
val startDate = endDate.minusMonths(1).plusDays(1) // 10th of previous month
val filter = TransactionService.TransactionsFilter(
dateFrom = startDate,
dateTo = endDate
)
for (space in spaces) {
val owner = space.owner
val refreshToken = owner.googleRefreshToken
if (refreshToken != null && refreshToken.isNotEmpty()) {
logger.info("Exporting for space \${space.id} (\${space.name}), owner \${owner.username}")
try {
val excelBytes = transactionService.generateExcel(space.id!!, filter)
val fileName = "Выгрузка_\${space.name}_\${startDate}_\${endDate}.xlsx"
googleDriveService.uploadExcelFile(refreshToken, fileName, excelBytes)
logger.info("Successfully exported to Google Drive for space \${space.id}")
} catch (e: Exception) {
logger.error("Failed to export space \${space.id} to Google Drive", e)
}
} else {
logger.debug("Skipping space \${space.id} because owner hasn't linked Google Drive")
}
}
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE finance.users ADD COLUMN google_refresh_token VARCHAR(255);