充值/订单(易支付 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>
63 lines
2.1 KiB
Go
63 lines
2.1 KiB
Go
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)
|
|
}
|