добавил приветственный бонус и README

This commit is contained in:
2025-12-26 07:02:01 +03:00
parent 5f35d7a75f
commit dfd855cb6e
4 changed files with 532 additions and 141 deletions

View File

@@ -72,27 +72,29 @@ func (r *pgRepository) GetUserByID(id uuid.UUID) (*account.User, error) {
// ConnectServer - Основная точка входа для добавления сервера
func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedPass, name string) (*account.RMSServer, error) {
// 1. Нормализация URL (удаляем слеш в конце, приводим к нижнему регистру)
// Важно: мы не удаляем http/https, так как iiko может работать и так и так, но обычно это разные эндпоинты.
// Для надежности уникальности можно вырезать протокол, но пока оставим как есть, просто тримминг.
cleanURL := strings.TrimRight(strings.ToLower(strings.TrimSpace(rawURL)), "/")
var server account.RMSServer
var created bool
err := r.db.Transaction(func(tx *gorm.DB) error {
// 2. Ищем, существует ли сервер с таким URL
err := tx.Where("base_url = ?", cleanURL).First(&server).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
if err == gorm.ErrRecordNotFound {
// --- СЦЕНАРИЙ 1: НОВЫЙ СЕРВЕР ---
// --- СЦЕНАРИЙ 1: НОВЫЙ СЕРВЕР (Приветственный бонус) ---
trialDays := 30
welcomeBalance := 10
paidUntil := time.Now().AddDate(0, 0, trialDays)
server = account.RMSServer{
BaseURL: cleanURL,
Name: name,
MaxUsers: 5, // Дефолтное ограничение
BaseURL: cleanURL,
Name: name,
MaxUsers: 5,
Balance: welcomeBalance,
PaidUntil: &paidUntil,
}
if err := tx.Create(&server).Error; err != nil {
return err
@@ -100,11 +102,9 @@ func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedP
created = true
} else {
// --- СЦЕНАРИЙ 2: СУЩЕСТВУЮЩИЙ СЕРВЕР ---
// Проверяем лимит пользователей
var userCount int64
tx.Model(&account.ServerUser{}).Where("server_id = ?", server.ID).Count(&userCount)
if userCount >= int64(server.MaxUsers) {
// Проверяем, может пользователь УЖЕ там есть? Тогда это не добавление, а обновление кредов
var exists int64
tx.Model(&account.ServerUser{}).Where("server_id = ? AND user_id = ?", server.ID, userID).Count(&exists)
if exists == 0 {
@@ -113,18 +113,24 @@ func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedP
}
}
// 3. Определяем роль
targetRole := account.RoleOperator
if created {
targetRole = account.RoleOwner
}
// 4. Создаем или обновляем связь с пользователем
// Сбрасываем активность других серверов
if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil {
return err
}
var existingLink account.ServerUser
err = tx.Where("server_id = ? AND user_id = ?", server.ID, userID).First(&existingLink).Error
if err == nil {
existingLink.Login = login
existingLink.EncryptedPassword = encryptedPass
existingLink.IsActive = true
return tx.Save(&existingLink).Error
}
userLink := account.ServerUser{
ServerID: server.ID,
UserID: userID,
@@ -133,22 +139,6 @@ func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedP
Login: login,
EncryptedPassword: encryptedPass,
}
// Upsert для связи (на случай если пользователь подключает уже подключенный сервер, обновляем пароль)
// Если пользователь уже был OWNER/ADMIN, роль НЕ понижаем.
// Поэтому используем хитрый Upsert: обновляем роль только если запись новая.
var existingLink account.ServerUser
err = tx.Where("server_id = ? AND user_id = ?", server.ID, userID).First(&existingLink).Error
if err == nil {
// Запись есть -> обновляем креды и делаем активным, роль НЕ меняем
existingLink.Login = login
existingLink.EncryptedPassword = encryptedPass
existingLink.IsActive = true
return tx.Save(&existingLink).Error
}
// Записи нет -> создаем с вычисленной ролью
return tx.Create(&userLink).Error
})

View File

@@ -156,7 +156,43 @@ func (bot *Bot) handleStartCommand(c tele.Context) error {
if payload != "" && strings.HasPrefix(payload, "invite_") {
return bot.handleInviteLink(c, strings.TrimPrefix(payload, "invite_"))
}
return bot.renderMainMenu(c)
welcomeTxt := "🚀 <b>RMSer — ваш умный ассистент для iiko</b>\n\n" +
"Больше не нужно вводить накладные вручную. Просто сфотографируйте чек, а я сделаю всё остальное.\n\n" +
"<b>Почему это удобно:</b>\n" +
"🧠 <b>Самообучение:</b> Сопоставьте товар один раз, и в следующий раз я узнаю его сам.\n" +
"⚙️ <b>Гибкая настройка:</b> Укажите склад по умолчанию и ограничьте область поиска товаров только нужными категориями.\n" +
"👥 <b>Работа в команде:</b> Приглашайте сотрудников, распределяйте роли и управляйте доступом прямо в Mini App.\n\n" +
"🎁 <b>Старт без риска:</b> Дарим 10 накладных на 30 дней каждому новому серверу!"
return bot.renderMainMenuWithText(c, welcomeTxt)
}
// Вспомогательный метод для рендера (чтобы не дублировать код меню)
func (bot *Bot) renderMainMenuWithText(c tele.Context, text string) error {
bot.fsm.Reset(c.Sender().ID)
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
activeServer, _ := bot.accountRepo.GetActiveServer(userDB.ID)
menu := &tele.ReplyMarkup{}
btnServers := menu.Data("🖥 Серверы", "nav_servers")
btnDicts := menu.Data("🔄 Справочники", "nav_dicts")
btnBalance := menu.Data("💰 Баланс", "nav_balance")
var rows []tele.Row
rows = append(rows, menu.Row(btnServers, btnDicts))
rows = append(rows, menu.Row(btnBalance))
if activeServer != nil {
role, _ := bot.accountRepo.GetUserRole(userDB.ID, activeServer.ID)
if role == account.RoleOwner || role == account.RoleAdmin {
btnApp := menu.WebApp("📱 Открыть приложение", &tele.WebApp{URL: bot.webAppURL})
rows = append(rows, menu.Row(btnApp))
}
}
menu.Inline(rows...)
return c.EditOrSend(text, menu, tele.ModeHTML)
}
func (bot *Bot) handleInviteLink(c tele.Context, serverIDStr string) error {
@@ -759,7 +795,7 @@ func (bot *Bot) handlePhoto(c tele.Context) error {
if err != nil {
return c.Send("Ошибка чтения файла.")
}
c.Send("⏳ Обрабатываю чек: создаю черновик и распознаю...")
c.Send("⏳ <b>ИИ анализирует документ...</b>\nОбрабатываю позиции и ищу совпадения в вашей базе iiko.", tele.ModeHTML)
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
draft, err := bot.ocrService.ProcessReceiptImage(ctx, userDB.ID, imgData)
@@ -777,14 +813,14 @@ func (bot *Bot) handlePhoto(c tele.Context) error {
fullURL := fmt.Sprintf("%s/invoice/%s", baseURL, draft.ID.String())
var msgText string
if matchedCount == len(draft.Items) {
msgText = fmt.Sprintf("✅ <b>Успех!</b> Все позиции (%d) распознаны.\n\nПереходите к созданию накладной.", len(draft.Items))
msgText = fmt.Sprintf("✅ <b>Все позиции распознаны!</b>\n\nВсе товары найдены в вашей номенклатуре. Проверьте количество (%d) и цену в приложении перед отправкой.", len(draft.Items))
} else {
msgText = fmt.Sprintf("⚠️ <b>Внимание!</b> Распознано %d из %d позиций.\n\nНекоторые товары требуют ручного сопоставления.", matchedCount, len(draft.Items))
msgText = fmt.Sprintf("⚠️ <b>Распознано позиций: %d из %d</b>\n\nОстальные товары я вижу впервые. Воспользуйтесь <b>удобным интерфейсом сопоставления</b> в приложении — я запомню ваш выбор навсегда.", matchedCount, len(draft.Items))
}
menu := &tele.ReplyMarkup{}
role, _ := bot.accountRepo.GetUserRole(userDB.ID, draft.RMSServerID)
if role != account.RoleOperator {
btnOpen := menu.WebApp("📝 Открыть накладную", &tele.WebApp{URL: fullURL})
btnOpen := menu.WebApp("📝 Открыть и сопоставить", &tele.WebApp{URL: fullURL})
menu.Inline(menu.Row(btnOpen))
} else {
msgText += "\n\n<i>(Редактирование доступно Администратору)</i>"
@@ -807,18 +843,32 @@ func (bot *Bot) handleConfirmNameNo(c tele.Context) error {
return c.EditOrSend("🏷 Хорошо, введите желаемое <b>название</b>:")
}
// Обновленный метод saveServerFinal (добавление уведомления о бонусе)
func (bot *Bot) saveServerFinal(c tele.Context, userID int64, serverName string) error {
ctx := bot.fsm.GetContext(userID)
userDB, _ := bot.accountRepo.GetOrCreateUser(userID, c.Sender().Username, "", "")
encPass, _ := bot.cryptoManager.Encrypt(ctx.TempPassword)
server, err := bot.accountRepo.ConnectServer(userDB.ID, ctx.TempURL, ctx.TempLogin, encPass, serverName)
if err != nil {
return c.Send("Ошибка подключения сервера: " + err.Error())
return c.Send("Ошибка подключения сервера: " + err.Error())
}
bot.fsm.Reset(userID)
role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID)
c.Send(fmt.Sprintf("✅ Сервер <b>%s</b> подключен!\nВаша роль: <b>%s</b>", server.Name, role), tele.ModeHTML)
successMsg := fmt.Sprintf("✅ Сервер <b>%s</b> успешно подключен!\nВаша роль: <b>%s</b>\n\n", server.Name, role)
// Проверяем, новый ли это сервер по балансу и дате создания (упрощенно для уведомления)
if server.Balance == 10 {
successMsg += "🎁 Вам начислен <b>приветственный бонус: 10 накладных</b> на 30 дней! Пользуйтесь с удовольствием.\n\n"
}
successMsg += "Начинаю первичную синхронизацию данных..."
c.Send(successMsg, tele.ModeHTML)
go bot.syncService.SyncAllData(userDB.ID)
return bot.renderMainMenu(c)
}