+ google drive
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
) {
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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> =
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/main/resources/db/migration/V36__.sql
Normal file
1
src/main/resources/db/migration/V36__.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE finance.users ADD COLUMN google_refresh_token VARCHAR(255);
|
||||
Reference in New Issue
Block a user