feat: 易支付积分充值 + 站内公告 + OpenAI 视频修复 + grok/adobe 失败处理
充值/订单(易支付 mapi): - 订单表 + 30 分钟自动取消;支付弹窗(二维码/跳转监控、倒计时、轮询、5s 倒计时关闭) - 系统设置可配:开关/商户ID/密钥/支付地址(根地址拼 /mapi)/支付方式/最低额/积分比例(默认 1元=100积分) - 异步通知 MD5 验签、幂等到账;用户累计充值;前台/后台订单页(筛选+搜索+分页,前后台分风格);用户管理累计充值列 站内公告: - Markdown 公告,登录用户首次访问/刷新弹出;内容哈希做版本,改了就重新推;管理员不弹;空内容=下线 OpenAI 视频(/v1/videos)修复: - /content 拿不到视频:grok 资源 URL 需鉴权,改为用生成账号 token 取流;adobe/runway 公开 URL 直代理(不存 RustFS) - size→分辨率用短边判定(1280x720 = 720p,之前误判 1080p 被拒) 失败处理: - grok 429「Too many requests」/403 anti-bot 改判临时错误(不再误封号),真额度耗尽才算 quota - adobe 视频 408 / system under load 归为临时错误 → tempAsDead 封号 其它: - 充值默认关闭;签到格子浅色可见;登录验证码按钮浅色可读;并发/账户信息展示 - 创作记录/画图台只显示画图台作品(排除 API);日志页 API 视频预览显示 — - 视频去画中画/下载/投屏(全局);图片缩略图改背景图规避 Edge 视觉搜索 - 订单/兑换码/配置/日志菜单文案与图标;充值版块样式 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"backend/internal/model"
|
||||
"backend/internal/repo"
|
||||
)
|
||||
|
||||
// AnnouncementService serves the site-wide 公告 (markdown). It tracks, per user,
|
||||
// the version they last dismissed so an updated announcement re-pops for everyone
|
||||
// who hasn't seen the new text. The "version" is a hash of the content, so saving
|
||||
// identical text doesn't re-notify; any edit does.
|
||||
type AnnouncementService struct {
|
||||
settings *repo.SiteSettingRepository
|
||||
users *repo.UserRepository
|
||||
}
|
||||
|
||||
func NewAnnouncementService(settings *repo.SiteSettingRepository, users *repo.UserRepository) *AnnouncementService {
|
||||
return &AnnouncementService{settings: settings, users: users}
|
||||
}
|
||||
|
||||
func announcementVersion(content string) string {
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256([]byte(strings.TrimSpace(content)))
|
||||
return hex.EncodeToString(sum[:8]) // 16 hex chars — plenty to detect a change
|
||||
}
|
||||
|
||||
// Content returns the raw markdown (admin editor).
|
||||
func (s *AnnouncementService) Content(ctx context.Context) string {
|
||||
v, _ := s.settings.GetValue(ctx, "announcement.content")
|
||||
return v
|
||||
}
|
||||
|
||||
// ForUser returns {content, version, seen} for the logged-in user.
|
||||
func (s *AnnouncementService) ForUser(ctx context.Context, user *model.User) map[string]any {
|
||||
content := s.Content(ctx)
|
||||
version := announcementVersion(content)
|
||||
// Admins never get the popup — they author/preview it in 系统设置 instead.
|
||||
seen := version == "" || (user != nil && (user.Role == "admin" || user.AnnouncementSeen == version))
|
||||
return map[string]any{
|
||||
"content": content,
|
||||
"version": version,
|
||||
"seen": seen,
|
||||
}
|
||||
}
|
||||
|
||||
// MarkSeen records that the user has dismissed the given version.
|
||||
func (s *AnnouncementService) MarkSeen(ctx context.Context, userID, version string) error {
|
||||
_, err := s.users.Update(ctx, userID, map[string]any{"announcement_seen": version})
|
||||
return err
|
||||
}
|
||||
|
||||
// Save persists new announcement markdown (admin). An empty string clears it.
|
||||
func (s *AnnouncementService) Save(ctx context.Context, content string) error {
|
||||
return s.settings.UpsertValue(ctx, "announcement.content", content)
|
||||
}
|
||||
Reference in New Issue
Block a user