logo和配置修改
This commit is contained in:
+5
-153
@@ -3,10 +3,8 @@ package authser
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/service/upstream"
|
||||
@@ -16,58 +14,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
registerPath = "/api/app/auth/register"
|
||||
loginPath = "/api/app/auth/login"
|
||||
resetPath = "/api/app/auth/resetPassword"
|
||||
guestPath = "/api/app/auth/guest"
|
||||
getInfoPath = "/api/app/auth/getInfo"
|
||||
profilePath = "/api/app/auth/profile"
|
||||
uploadPath = "/api/app/upload/image"
|
||||
cookieMaxAge = 7 * 24 * 3600
|
||||
)
|
||||
|
||||
// Register 调用上游注册接口。
|
||||
func Register(c *gin.Context, req RegisterReq) (*APIEnvelope, *LoginResp, error) {
|
||||
var env APIEnvelope
|
||||
if _, err := upstream.PostJSON(c, registerPath, req, &env); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return &env, nil, nil
|
||||
}
|
||||
user, err := decodeLoginData(env.Data)
|
||||
if err != nil {
|
||||
return &env, nil, err
|
||||
}
|
||||
return &env, user, nil
|
||||
}
|
||||
|
||||
// ResetPassword 通过账号和邮箱校验后重置密码。
|
||||
func ResetPassword(c *gin.Context, req ResetPasswordReq) (*APIEnvelope, error) {
|
||||
var env APIEnvelope
|
||||
if _, err := upstream.PostJSON(c, resetPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// Login 调用上游登录接口。
|
||||
func Login(c *gin.Context, req LoginReq) (*APIEnvelope, *LoginResp, error) {
|
||||
var env APIEnvelope
|
||||
if _, err := upstream.PostJSON(c, loginPath, req, &env); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return &env, nil, nil
|
||||
}
|
||||
user, err := decodeLoginData(env.Data)
|
||||
if err != nil {
|
||||
return &env, nil, err
|
||||
}
|
||||
return &env, user, nil
|
||||
}
|
||||
|
||||
// GuestLogin 调用上游游客登录(按设备 ID 创建或找回游客账号)。
|
||||
// GuestLogin 调用上游游客登录,站点不提供注册/登录,上游内容接口依赖游客会话。
|
||||
func GuestLogin(c *gin.Context, req GuestLoginReq) (*APIEnvelope, *LoginResp, error) {
|
||||
var env APIEnvelope
|
||||
if _, err := upstream.PostJSON(c, guestPath, req, &env); err != nil {
|
||||
@@ -83,47 +34,6 @@ func GuestLogin(c *gin.Context, req GuestLoginReq) (*APIEnvelope, *LoginResp, er
|
||||
return &env, user, nil
|
||||
}
|
||||
|
||||
// GetInfo 通过 token 拉取最新用户信息。
|
||||
func GetInfo(c *gin.Context) (*APIEnvelope, *LoginResp, error) {
|
||||
var env APIEnvelope
|
||||
if err := upstream.GetJSON(c, getInfoPath, &env); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return &env, nil, nil
|
||||
}
|
||||
user, err := decodeLoginData(env.Data)
|
||||
if err != nil {
|
||||
return &env, nil, err
|
||||
}
|
||||
return &env, user, nil
|
||||
}
|
||||
|
||||
// UpdateProfile 修改资料(昵称 / 头像等)。
|
||||
func UpdateProfile(c *gin.Context, req UpdateProfileReq) (*APIEnvelope, error) {
|
||||
var env APIEnvelope
|
||||
if _, err := upstream.PostJSON(c, profilePath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// UploadImage 上传图片;头像场景不加水印。
|
||||
func UploadImage(c *gin.Context, fileName string, r io.Reader) (*ImageUploadResp, error) {
|
||||
var resp ImageUploadResp
|
||||
_, err := upstream.PostMultipart(c, uploadPath, map[string]string{
|
||||
"watermark": "0",
|
||||
}, &upstream.MultipartFile{
|
||||
FieldName: "file",
|
||||
FileName: fileName,
|
||||
Reader: r,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func decodeLoginData(raw json.RawMessage) (*LoginResp, error) {
|
||||
if len(raw) == 0 || string(raw) == "null" || string(raw) == `""` {
|
||||
return nil, fmt.Errorf("empty login data")
|
||||
@@ -135,7 +45,7 @@ func decodeLoginData(raw json.RawMessage) (*LoginResp, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// SetAuthCookies 将登录/注册返回写入 cookie。
|
||||
// SetAuthCookies 写入游客会话 cookie。
|
||||
func SetAuthCookies(c *gin.Context, user *LoginResp) {
|
||||
if c == nil || user == nil {
|
||||
return
|
||||
@@ -144,33 +54,13 @@ func SetAuthCookies(c *gin.Context, user *LoginResp) {
|
||||
if strings.TrimSpace(user.Token) != "" {
|
||||
setCookie(c, "token", user.Token, secure)
|
||||
}
|
||||
setCookie(c, "userId", strconv.FormatInt(user.UserID, 10), secure)
|
||||
setCookie(c, "accountNo", user.AccountNo, secure)
|
||||
setCookie(c, "nickName", user.NickName, secure)
|
||||
setCookie(c, "introduction", user.Introduction, secure)
|
||||
setCookie(c, "avatar", user.Avatar, secure)
|
||||
setCookie(c, "email", user.Email, secure)
|
||||
setCookie(c, "inviteCode", user.InviteCode, secure)
|
||||
setCookie(c, "recharge", strconv.FormatInt(user.Recharge, 10), secure)
|
||||
setCookie(c, "vipType", strconv.Itoa(user.VipType), secure)
|
||||
setCookie(c, "currentVipExpire", user.CurrentVipExpire, secure)
|
||||
setCookie(c, "showCardId", strconv.FormatInt(user.ShowCardID, 10), secure)
|
||||
setCookie(c, "showCardName", user.ShowCardName, secure)
|
||||
setCookie(c, "userType", strconv.Itoa(user.Type), secure)
|
||||
isReg := "0"
|
||||
if user.IsRegister || user.Type == 1 {
|
||||
isReg = "1"
|
||||
}
|
||||
setCookie(c, "isRegister", isReg, secure)
|
||||
if sid := useragent.SIDFromRequest(c.Request); sid != "" {
|
||||
setCookie(c, "sid", sid, secure)
|
||||
}
|
||||
}
|
||||
|
||||
func setCookie(c *gin.Context, name, value string, secure bool) {
|
||||
// Cookie Value 仅允许 ASCII;中文昵称等需 URL 编码。
|
||||
// 使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致;
|
||||
// 避免 QueryEscape 的空格→+,否则 JS decodeURIComponent 会留下字面量 "+"。
|
||||
// Cookie Value 仅允许 ASCII;使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致。
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: name,
|
||||
Value: url.PathEscape(value),
|
||||
@@ -199,7 +89,7 @@ func cookieValue(c *gin.Context, name string) string {
|
||||
return raw
|
||||
}
|
||||
|
||||
// ClearAuthCookies 清除登录相关 cookie。
|
||||
// ClearAuthCookies 清除会话 cookie,同时清理旧版账号体系遗留的用户信息 cookie。
|
||||
func ClearAuthCookies(c *gin.Context) {
|
||||
for _, name := range authCookieNames() {
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
@@ -218,44 +108,6 @@ func authCookieNames() []string {
|
||||
"token", "_token_", "sid", "_s_token_",
|
||||
"userId", "accountNo", "nickName", "introduction", "avatar",
|
||||
"email", "inviteCode", "recharge", "vipType", "currentVipExpire", "showCardId", "showCardName",
|
||||
"userType", "isRegister",
|
||||
"userType", "isRegister", "guest_skip_auto",
|
||||
}
|
||||
}
|
||||
|
||||
// AuthFromCookies 从 cookie 读取登录态,供 SSR 头部 / 个人中心展示。
|
||||
func AuthFromCookies(c *gin.Context) (loggedIn bool, userName, avatar, email string) {
|
||||
token := useragent.TokenFromRequest(c.Request)
|
||||
if token == "" {
|
||||
return false, "", "", ""
|
||||
}
|
||||
userName = cookieValue(c, "nickName")
|
||||
if userName == "" {
|
||||
userName = cookieValue(c, "accountNo")
|
||||
}
|
||||
avatar = usableAvatarURL(cookieValue(c, "avatar"))
|
||||
email = cookieValue(c, "email")
|
||||
return true, userName, avatar, email
|
||||
}
|
||||
|
||||
// IsRegisteredFromCookies 是否正式注册账号(非游客)。
|
||||
func IsRegisteredFromCookies(c *gin.Context) bool {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
return false
|
||||
}
|
||||
if cookieValue(c, "isRegister") == "1" {
|
||||
return true
|
||||
}
|
||||
return cookieValue(c, "userType") == "1"
|
||||
}
|
||||
|
||||
func usableAvatarURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") || strings.HasPrefix(raw, "/static/") {
|
||||
return raw
|
||||
}
|
||||
// 无域名的相对路径暂不可用(解密拉取需要完整 URL)
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -16,24 +15,17 @@ import (
|
||||
)
|
||||
|
||||
const guestDevIDCookie = "guest_dev_id"
|
||||
const guestSkipAutoCookie = "guest_skip_auto"
|
||||
|
||||
// EnsureGuestOnEnter 页面进入时:无 token(未自己登录)则走游客登录并写 cookie。
|
||||
// 对应上游 POST /api/app/auth/guest。成功后会把会话补进当前 Request,便于同一次 SSR 读到登录态。
|
||||
// 手动退出后带 guest_skip_auto 时跳过本次;清掉标记后,下次进页再自动游客。
|
||||
// EnsureGuestOnEnter 页面进入时:无 token 则走游客登录并写 cookie。
|
||||
// 对应上游 POST /api/app/auth/guest。成功后会把会话补进当前 Request,便于同一次 SSR 直接使用。
|
||||
func EnsureGuestOnEnter(c *gin.Context) *LoginResp {
|
||||
if c == nil || c.Request == nil {
|
||||
return nil
|
||||
}
|
||||
// 已有任意会话(游客或正式账号)则跳过
|
||||
// 已有会话则跳过
|
||||
if useragent.TokenFromRequest(c.Request) != "" {
|
||||
return nil
|
||||
}
|
||||
// 手动退出:本次进页不自动游客,并清除标记,供下次进页恢复
|
||||
if IsGuestSkipAuto(c) {
|
||||
ClearGuestSkipAuto(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
devID := ensureGuestDevID(c)
|
||||
env, user, err := GuestLogin(c, GuestLoginReq{
|
||||
@@ -55,25 +47,10 @@ func EnsureGuestOnEnter(c *gin.Context) *LoginResp {
|
||||
}
|
||||
|
||||
SetAuthCookies(c, user)
|
||||
ClearGuestSkipAuto(c)
|
||||
patchAuthCookiesToRequest(c, user)
|
||||
patchRequestCookie(c, "token", user.Token)
|
||||
return user
|
||||
}
|
||||
|
||||
// RefreshGuestSession 游客会话失效时(如上游 6001 用户不存在)重新游客登录。
|
||||
// 正式注册账号不处理,避免把用户踢成游客。
|
||||
func RefreshGuestSession(c *gin.Context) *LoginResp {
|
||||
if c == nil || c.Request == nil {
|
||||
return nil
|
||||
}
|
||||
if IsRegisteredFromCookies(c) {
|
||||
return nil
|
||||
}
|
||||
dropRequestCookie(c, "token")
|
||||
dropRequestCookie(c, "_token_")
|
||||
return EnsureGuestOnEnter(c)
|
||||
}
|
||||
|
||||
func dropRequestCookie(c *gin.Context, name string) {
|
||||
if c == nil || c.Request == nil || name == "" {
|
||||
return
|
||||
@@ -88,43 +65,6 @@ func dropRequestCookie(c *gin.Context, name string) {
|
||||
c.Request.Header.Set("Cookie", strings.Join(parts, "; "))
|
||||
}
|
||||
|
||||
// MarkGuestSkipAuto 手动退出后标记:跳过紧接着的一次自动游客登录。
|
||||
func MarkGuestSkipAuto(c *gin.Context) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
secure := c.Request != nil && c.Request.TLS != nil
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: guestSkipAutoCookie,
|
||||
Value: "1",
|
||||
Path: "/",
|
||||
MaxAge: 0, // session cookie
|
||||
HttpOnly: false,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
// IsGuestSkipAuto 是否处于「手动退出后跳过自动游客」状态。
|
||||
func IsGuestSkipAuto(c *gin.Context) bool {
|
||||
return cookieValue(c, guestSkipAutoCookie) == "1"
|
||||
}
|
||||
|
||||
// ClearGuestSkipAuto 清除手动退出跳过标记。
|
||||
func ClearGuestSkipAuto(c *gin.Context) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: guestSkipAutoCookie,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: false,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
func ensureGuestDevID(c *gin.Context) string {
|
||||
if id := cookieValue(c, guestDevIDCookie); id != "" {
|
||||
return id
|
||||
@@ -164,31 +104,6 @@ func guestChannel(c *gin.Context) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func patchAuthCookiesToRequest(c *gin.Context, user *LoginResp) {
|
||||
if c == nil || user == nil {
|
||||
return
|
||||
}
|
||||
patchRequestCookie(c, "token", user.Token)
|
||||
patchRequestCookie(c, "userId", strconv.FormatInt(user.UserID, 10))
|
||||
patchRequestCookie(c, "accountNo", user.AccountNo)
|
||||
patchRequestCookie(c, "nickName", user.NickName)
|
||||
patchRequestCookie(c, "introduction", user.Introduction)
|
||||
patchRequestCookie(c, "avatar", user.Avatar)
|
||||
patchRequestCookie(c, "email", user.Email)
|
||||
patchRequestCookie(c, "inviteCode", user.InviteCode)
|
||||
patchRequestCookie(c, "recharge", strconv.FormatInt(user.Recharge, 10))
|
||||
patchRequestCookie(c, "vipType", strconv.Itoa(user.VipType))
|
||||
patchRequestCookie(c, "currentVipExpire", user.CurrentVipExpire)
|
||||
patchRequestCookie(c, "showCardId", strconv.FormatInt(user.ShowCardID, 10))
|
||||
patchRequestCookie(c, "showCardName", user.ShowCardName)
|
||||
patchRequestCookie(c, "userType", strconv.Itoa(user.Type))
|
||||
isReg := "0"
|
||||
if user.IsRegister || user.Type == 1 {
|
||||
isReg = "1"
|
||||
}
|
||||
patchRequestCookie(c, "isRegister", isReg)
|
||||
}
|
||||
|
||||
func patchRequestCookie(c *gin.Context, name, value string) {
|
||||
if c == nil || c.Request == nil || name == "" {
|
||||
return
|
||||
|
||||
@@ -2,7 +2,7 @@ package authser
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// TokenExpiredCode 上游约定:token 失效,需重新登录。
|
||||
// TokenExpiredCode 上游约定:token 失效。
|
||||
const TokenExpiredCode = 1000
|
||||
|
||||
// IsSessionExpiredCode 是否为会话失效类业务码。
|
||||
@@ -15,31 +15,7 @@ func IsSessionExpiredCode(code int) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// OnSessionExpired 处理上游会话失效。
|
||||
// retry=true:游客已重新登录,调用方应重试原请求;
|
||||
// needLogin=true:注册账号已清 cookie,调用方应跳转登录弹窗/页。
|
||||
func OnSessionExpired(c *gin.Context, code int) (retry, needLogin bool) {
|
||||
if c == nil || !IsSessionExpiredCode(code) {
|
||||
return false, false
|
||||
}
|
||||
if IsRegisteredFromCookies(c) {
|
||||
ClearAuthCookies(c)
|
||||
dropRequestCookie(c, "token")
|
||||
dropRequestCookie(c, "_token_")
|
||||
return false, true
|
||||
}
|
||||
if RefreshGuestSession(c) != nil {
|
||||
return true, false
|
||||
}
|
||||
ClearAuthCookies(c)
|
||||
dropRequestCookie(c, "token")
|
||||
dropRequestCookie(c, "_token_")
|
||||
return false, false
|
||||
}
|
||||
|
||||
// RecoverContentSession 用于允许游客访问的内容读取接口。
|
||||
// 正式账号或游客 token 失效后,清理旧登录态、重新建立游客会话,调用方随后重试一次原请求。
|
||||
// 写操作仍应使用 OnSessionExpired,避免把用户操作静默降级为游客操作。
|
||||
// RecoverContentSession 会话失效后清理旧 cookie 并重新建立游客会话,调用方随后重试一次原请求。
|
||||
func RecoverContentSession(c *gin.Context, code int) bool {
|
||||
if c == nil || !IsSessionExpiredCode(code) {
|
||||
return false
|
||||
@@ -48,7 +24,5 @@ func RecoverContentSession(c *gin.Context, code int) bool {
|
||||
for _, name := range authCookieNames() {
|
||||
dropRequestCookie(c, name)
|
||||
}
|
||||
dropRequestCookie(c, guestSkipAutoCookie)
|
||||
ClearGuestSkipAuto(c)
|
||||
return EnsureGuestOnEnter(c) != nil
|
||||
}
|
||||
|
||||
@@ -2,31 +2,6 @@ package authser
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// RegisterReq 对应 swagger authapi.registerReq
|
||||
type RegisterReq struct {
|
||||
AccountNo string `json:"accountNo"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Password string `json:"password"`
|
||||
AgentInviteCode string `json:"agentInviteCode,omitempty"`
|
||||
DevID string `json:"devID,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
// LoginReq 对应 swagger authapi.loginReq
|
||||
type LoginReq struct {
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
DevID string `json:"devID,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
// ResetPasswordReq 对应 swagger authapi.resetPasswordReq。
|
||||
type ResetPasswordReq struct {
|
||||
AccountNo string `json:"accountNo"`
|
||||
Email string `json:"email"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
// GuestLoginReq 对应 swagger authser.GuestLoginReq
|
||||
// POST /api/app/auth/guest
|
||||
type GuestLoginReq struct {
|
||||
@@ -36,39 +11,10 @@ type GuestLoginReq struct {
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateProfileReq 对应 swagger authapi.updateProfileReq
|
||||
type UpdateProfileReq struct {
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Introduction string `json:"introduction,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
OldPassword string `json:"oldPassword,omitempty"`
|
||||
NewPassword string `json:"newPassword,omitempty"`
|
||||
PrivacyMode *bool `json:"privacyMode,omitempty"`
|
||||
}
|
||||
|
||||
// LoginResp 对应 swagger authser.LoginResp
|
||||
// GET /api/app/auth/getInfo、登录/注册/游客成功 data。
|
||||
// LoginResp 游客登录成功 data(只取会话所需字段)。
|
||||
type LoginResp struct {
|
||||
Token string `json:"token"`
|
||||
UserID int64 `json:"userId"`
|
||||
AccountNo string `json:"accountNo"`
|
||||
Email string `json:"email"`
|
||||
NickName string `json:"nickName"`
|
||||
Introduction string `json:"introduction"` // 简介
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
InviteCode string `json:"inviteCode"` // 邀请码
|
||||
VipType int `json:"vipType"` // 0没卡 1月卡 2年卡
|
||||
CurrentVipExpire string `json:"currentVipExpire"` // VIP 过期时间,需结合 vipType 判断是否有效
|
||||
ShowCardID int64 `json:"showCardId"` // 最新会员卡 id
|
||||
ShowCardName string `json:"showCardName"` // 最新会员卡名称,如「1个月会员」
|
||||
Status int `json:"status"` // 0正常 1封禁 2禁言
|
||||
Type int `json:"type"` // 身份类型:0游客 1普通用户
|
||||
IsNewUser bool `json:"isNewUser"` // 本次是否新建游客
|
||||
IsRegister bool `json:"isRegister"` // 是否已注册
|
||||
LoginType int `json:"loginType"` // 登录方式
|
||||
PrivacyMode bool `json:"privacyMode"` // 隐私模式
|
||||
Score int64 `json:"score"` // 积分
|
||||
Recharge int64 `json:"recharge"` // 金币余额
|
||||
Token string `json:"token"`
|
||||
UserID int64 `json:"userId"`
|
||||
}
|
||||
|
||||
// APIEnvelope 上游统一响应壳。Data 在失败时可能是 string/null。
|
||||
@@ -78,17 +24,3 @@ type APIEnvelope struct {
|
||||
Tip string `json:"tip"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// ImageUploadData 对应 swagger uploadser.ImageUploadData
|
||||
type ImageUploadData struct {
|
||||
ID string `json:"id"`
|
||||
SourceNameFs string `json:"sourceNameFs"`
|
||||
}
|
||||
|
||||
// ImageUploadResp 对应 swagger uploadser.ImageUploadResp
|
||||
type ImageUploadResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Tip string `json:"tip"`
|
||||
Data *ImageUploadData `json:"data"`
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SettingPageVM 账号设置页展示模型。
|
||||
type SettingPageVM struct {
|
||||
UserID int64
|
||||
AccountNo string
|
||||
NickName string
|
||||
Introduction string
|
||||
Email string
|
||||
EmailMasked string
|
||||
EmailBound bool
|
||||
Avatar string // 可用头像 URL;空则模板用默认图
|
||||
InviteCode string
|
||||
PrivacyMode bool
|
||||
VipType int
|
||||
VipExpire string
|
||||
Recharge int64
|
||||
Score int64
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// BuildSettingVM 由 LoginResp 构建设置页 VM。
|
||||
func BuildSettingVM(user *LoginResp) SettingPageVM {
|
||||
if user == nil {
|
||||
return SettingPageVM{}
|
||||
}
|
||||
nick := strings.TrimSpace(user.NickName)
|
||||
account := strings.TrimSpace(user.AccountNo)
|
||||
email := strings.TrimSpace(user.Email)
|
||||
intro := strings.TrimSpace(user.Introduction)
|
||||
display := nick
|
||||
if display == "" {
|
||||
display = account
|
||||
}
|
||||
if display == "" {
|
||||
display = "用户"
|
||||
}
|
||||
return SettingPageVM{
|
||||
UserID: user.UserID,
|
||||
AccountNo: account,
|
||||
NickName: nick,
|
||||
Introduction: intro,
|
||||
Email: email,
|
||||
EmailMasked: MaskEmail(email),
|
||||
EmailBound: email != "",
|
||||
Avatar: usableAvatarURL(user.Avatar),
|
||||
InviteCode: strings.TrimSpace(user.InviteCode),
|
||||
PrivacyMode: user.PrivacyMode,
|
||||
VipType: user.VipType,
|
||||
VipExpire: strings.TrimSpace(user.CurrentVipExpire),
|
||||
Recharge: user.Recharge,
|
||||
Score: user.Score,
|
||||
DisplayName: display,
|
||||
}
|
||||
}
|
||||
|
||||
// MaskEmail 邮箱脱敏,如 ab***@mail.com。
|
||||
func MaskEmail(email string) string {
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
return ""
|
||||
}
|
||||
at := strings.LastIndex(email, "@")
|
||||
if at <= 0 || at == len(email)-1 {
|
||||
return email
|
||||
}
|
||||
local := email[:at]
|
||||
domain := email[at:]
|
||||
runes := []rune(local)
|
||||
if len(runes) <= 2 {
|
||||
return string(runes[:1]) + "***" + domain
|
||||
}
|
||||
return string(runes[:2]) + "***" + domain
|
||||
}
|
||||
|
||||
// FormatUserID 展示用 ID。
|
||||
func FormatUserID(id int64) string {
|
||||
if id <= 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d", id)
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func parseVipExpireMs(raw string) int64 {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return 0
|
||||
}
|
||||
if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||
if n > 0 && n < 1e12 {
|
||||
return n * 1000
|
||||
}
|
||||
return n
|
||||
}
|
||||
layouts := []string{
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05Z07:00",
|
||||
"2006-01-02 15:04:05",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.Parse(layout, raw); err == nil {
|
||||
return t.UnixMilli()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// UserIDFromCookies 读取 cookie userId。
|
||||
func UserIDFromCookies(c *gin.Context) int64 {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
n, _ := strconv.ParseInt(strings.TrimSpace(cookieValue(c, "userId")), 10, 64)
|
||||
if n < 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// VipTypeFromCookies 读取 cookie vipType。
|
||||
func VipTypeFromCookies(c *gin.Context) int {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
n, _ := strconv.Atoi(strings.TrimSpace(cookieValue(c, "vipType")))
|
||||
return n
|
||||
}
|
||||
|
||||
// VipTypeLabel vipType 文案:0普通 1月卡 2年卡(与 91ly / 编辑资料注释一致)。
|
||||
func VipTypeLabel(vipType int) string {
|
||||
switch vipType {
|
||||
case 1:
|
||||
return "月卡会员"
|
||||
case 2:
|
||||
return "年卡会员"
|
||||
default:
|
||||
return "普通会员"
|
||||
}
|
||||
}
|
||||
|
||||
// ShowCardNameFromCookies 读取 cookie showCardName。
|
||||
func ShowCardNameFromCookies(c *gin.Context) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(cookieValue(c, "showCardName"))
|
||||
}
|
||||
|
||||
// ShowCardIDFromCookies 读取 cookie showCardId。
|
||||
func ShowCardIDFromCookies(c *gin.Context) int64 {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
n, _ := strconv.ParseInt(strings.TrimSpace(cookieValue(c, "showCardId")), 10, 64)
|
||||
if n < 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// VipDisplayLabel 优先使用 getInfo.showCardName,否则回退 vipType 文案。
|
||||
func VipDisplayLabel(showCardName string, vipType int) string {
|
||||
if s := strings.TrimSpace(showCardName); s != "" {
|
||||
return s
|
||||
}
|
||||
return VipTypeLabel(vipType)
|
||||
}
|
||||
|
||||
// VipDisplayLabelFromCookies 从 cookie 组装会员展示名。
|
||||
func VipDisplayLabelFromCookies(c *gin.Context) string {
|
||||
return VipDisplayLabel(ShowCardNameFromCookies(c), VipTypeFromCookies(c))
|
||||
}
|
||||
|
||||
// IsActiveVipUser 根据 vipType + currentVipExpire 判断是否有效会员。
|
||||
func IsActiveVipUser(vipType int, currentVipExpire string) bool {
|
||||
if vipType == 0 {
|
||||
return false
|
||||
}
|
||||
expireMs := parseVipExpireMs(currentVipExpire)
|
||||
if expireMs <= 0 {
|
||||
return false
|
||||
}
|
||||
return expireMs > time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// FormatVipExpireText 格式化到期时间展示。
|
||||
func FormatVipExpireText(currentVipExpire string) string {
|
||||
ms := parseVipExpireMs(currentVipExpire)
|
||||
if ms <= 0 {
|
||||
return ""
|
||||
}
|
||||
return time.UnixMilli(ms).In(time.Local).Format("有效期至 2006/01/02")
|
||||
}
|
||||
|
||||
// VipExpireTextFromCookies 格式化到期时间展示。
|
||||
func VipExpireTextFromCookies(c *gin.Context) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
return FormatVipExpireText(cookieValue(c, "currentVipExpire"))
|
||||
}
|
||||
|
||||
// IsActiveVipFromCookies vipType≠0 且 currentVipExpire 未过期(与 91ly 一致)。
|
||||
func IsActiveVipFromCookies(c *gin.Context) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
return IsActiveVipUser(VipTypeFromCookies(c), cookieValue(c, "currentVipExpire"))
|
||||
}
|
||||
|
||||
// RechargeFromCookies 读取 cookie recharge(金币余额)。
|
||||
func RechargeFromCookies(c *gin.Context) int64 {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
n, _ := strconv.ParseInt(strings.TrimSpace(cookieValue(c, "recharge")), 10, 64)
|
||||
if n < 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// SetRechargeCookie 更新金币余额 cookie(解锁/充值后同步)。
|
||||
func SetRechargeCookie(c *gin.Context, balance int64) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if balance < 0 {
|
||||
balance = 0
|
||||
}
|
||||
secure := c.Request != nil && c.Request.TLS != nil
|
||||
setCookie(c, "recharge", strconv.FormatInt(balance, 10), secure)
|
||||
}
|
||||
|
||||
// FormatRecharge 金币余额千分位展示。
|
||||
func FormatRecharge(n int64) string {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
s := strconv.FormatInt(n, 10)
|
||||
if len(s) <= 3 {
|
||||
return s
|
||||
}
|
||||
var b strings.Builder
|
||||
rem := len(s) % 3
|
||||
if rem > 0 {
|
||||
b.WriteString(s[:rem])
|
||||
if len(s) > rem {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
}
|
||||
for i := rem; i < len(s); i += 3 {
|
||||
b.WriteString(s[i : i+3])
|
||||
if i+3 < len(s) {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/upstream"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const likeSetPath = "/api/app/comment/like/set"
|
||||
|
||||
// LikeSet 设置评论点赞/取消点赞。
|
||||
func LikeSet(c *gin.Context, req CommentLikeSetReq) (*APIEnvelope[CommentLikeSetResp], error) {
|
||||
if req.CommentID <= 0 {
|
||||
return nil, fmt.Errorf("comment like invalid commentId")
|
||||
}
|
||||
var env APIEnvelope[CommentLikeSetResp]
|
||||
if _, err := upstream.PostJSON(c, likeSetPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
"videoweb/app/service/upstream"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const sendPath = "/api/app/comment/send"
|
||||
|
||||
// Send 发布评论(一级 / 二级)。
|
||||
func Send(c *gin.Context, req CommentSendReq) (*APIEnvelope[any], error) {
|
||||
req.Text = strings.TrimSpace(req.Text)
|
||||
if req.ObjectID <= 0 {
|
||||
return nil, fmt.Errorf("comment send invalid objectId")
|
||||
}
|
||||
if req.ObjectType != ObjectTypeChapter {
|
||||
req.ObjectType = ObjectTypePlaylet
|
||||
}
|
||||
if req.Text == "" {
|
||||
return nil, fmt.Errorf("comment send empty text")
|
||||
}
|
||||
if utf8.RuneCountInString(req.Text) > 200 {
|
||||
return nil, fmt.Errorf("comment send text too long")
|
||||
}
|
||||
if req.ParentsID < 0 {
|
||||
req.ParentsID = 0
|
||||
}
|
||||
if req.ReplyCommentID < 0 {
|
||||
req.ReplyCommentID = 0
|
||||
}
|
||||
// 二级评论:未显式指定 replyCommentId 时默认回复父评论
|
||||
if req.ParentsID > 0 && req.ReplyCommentID <= 0 {
|
||||
req.ReplyCommentID = req.ParentsID
|
||||
}
|
||||
|
||||
var env APIEnvelope[any]
|
||||
if _, err := upstream.PostJSON(c, sendPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// ResolveTrackerAppID 优先 yaml,否则固定 JHA-1043。
|
||||
func ResolveTrackerAppID() string {
|
||||
if id := strings.TrimSpace(appg.Conf.Tracker.AppID); id != "" {
|
||||
return id
|
||||
}
|
||||
return pubdef.TrackerAppId()
|
||||
}
|
||||
|
||||
// ResolveReportSDKConfig 上游 reportSdkConfig 优先,本地 yaml 兜底。
|
||||
func ResolveReportSDKConfig(fromAPI string) string {
|
||||
if v := strings.TrimSpace(fromAPI); v != "" {
|
||||
return v
|
||||
}
|
||||
return strings.TrimSpace(appg.Conf.Tracker.EncryptedConfig)
|
||||
}
|
||||
|
||||
// ApplyTrackerFallback 补齐 AppConfigVM 中的上报密文。
|
||||
func ApplyTrackerFallback(vm AppConfigVM) AppConfigVM {
|
||||
vm.ReportSDKConfig = ResolveReportSDKConfig(vm.ReportSDKConfig)
|
||||
return vm
|
||||
}
|
||||
@@ -52,16 +52,14 @@ type FooterContact struct {
|
||||
|
||||
// ConfigInfoResp 对应 swagger proto.ConfigInfoResp
|
||||
// GET /api/app/config/info
|
||||
// reportSdkConfig 与 proto.ConfigInfo 对齐;上游若尚未下发则为空,可由本地 yaml 兜底。
|
||||
// config 为上游实际下发的完整站点配置(swagger 未完整声明,但含 potaoGroup 等字段)。
|
||||
type ConfigInfoResp struct {
|
||||
NewAddressInfo NewAddressInfo `json:"newAddressInfo"`
|
||||
Config ConfigInfo `json:"config"`
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
SeoInfo SeoInfo `json:"seoInfo"`
|
||||
SeoNavInfo SeoNavInfo `json:"seoNavInfo"`
|
||||
NavPageList []NavPageItem `json:"navPageList"`
|
||||
ReportSDKConfig string `json:"reportSdkConfig"`
|
||||
NewAddressInfo NewAddressInfo `json:"newAddressInfo"`
|
||||
Config ConfigInfo `json:"config"`
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
SeoInfo SeoInfo `json:"seoInfo"`
|
||||
SeoNavInfo SeoNavInfo `json:"seoNavInfo"`
|
||||
NavPageList []NavPageItem `json:"navPageList"`
|
||||
}
|
||||
|
||||
// ConfigInfo 对应上游 data.config(proto.ConfigInfo 子集,按页面需要取用)。
|
||||
|
||||
@@ -24,7 +24,6 @@ type AppConfigVM struct {
|
||||
SeoInfo SeoInfo `json:"seoInfo,omitempty"`
|
||||
SeoNavInfo SeoNavInfo `json:"seoNavInfo,omitempty"`
|
||||
NavPageList []NavPageItem `json:"navPageList,omitempty"`
|
||||
ReportSDKConfig string `json:"reportSdkConfig,omitempty"`
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
DetailTopList []Advertisement `json:"detailTopList,omitempty"`
|
||||
NineGridList []Advertisement `json:"nineGridList,omitempty"`
|
||||
@@ -54,7 +53,6 @@ func BuildAppConfigVM(info *ConfigInfoResp) AppConfigVM {
|
||||
OfficialAppAddr: sanitizeURL(addr.OfficialAppAddr),
|
||||
PDFAddr: sanitizeURL(addr.PDFAddr),
|
||||
TwitterAddr: sanitizeURL(addr.TwitterAddr),
|
||||
ReportSDKConfig: strings.TrimSpace(info.ReportSDKConfig),
|
||||
NavPageList: buildNavPageList(info.NavPageList),
|
||||
Advitise: info.Advitise,
|
||||
DetailTopList: buildAdvertisementList(info.Advitise.DetailTopList),
|
||||
|
||||
@@ -139,20 +139,6 @@ func (c *PlayletChapterInfoResp) AllowWatch() bool {
|
||||
return strings.TrimSpace(c.VideoURL) != "" && c.PayType == 0
|
||||
}
|
||||
|
||||
// UnlockCost 金币解锁消耗:优先 costCoins。
|
||||
func (c *PlayletChapterInfoResp) UnlockCost() int64 {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
if c.CostCoins > 0 {
|
||||
return c.CostCoins
|
||||
}
|
||||
if c.SingleCoins > 0 {
|
||||
return int64(c.SingleCoins)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AllowWatch 详情选集是否可直达播放(优先 canWatch)。
|
||||
func (c PlayletChapterData) AllowWatch() bool {
|
||||
if c.CanWatch {
|
||||
@@ -166,33 +152,3 @@ func (c PlayletChapterData) AllowWatch() bool {
|
||||
}
|
||||
return c.PayType == 0
|
||||
}
|
||||
|
||||
// NeedCoinGate 是否弹出金币解锁。
|
||||
func (c PlayletChapterData) NeedCoinGate() bool {
|
||||
if c.AllowWatch() {
|
||||
return false
|
||||
}
|
||||
if c.NeedUnlock {
|
||||
return true
|
||||
}
|
||||
return c.PayType == 1
|
||||
}
|
||||
|
||||
// NeedVipGate 是否弹出 VIP 开通。
|
||||
func (c PlayletChapterData) NeedVipGate() bool {
|
||||
if c.AllowWatch() || c.NeedCoinGate() {
|
||||
return false
|
||||
}
|
||||
if c.NeedVip {
|
||||
return true
|
||||
}
|
||||
return c.PayType == 2
|
||||
}
|
||||
|
||||
// UnlockCost 金币解锁消耗:优先 costCoins。
|
||||
func (c PlayletChapterData) UnlockCost() int {
|
||||
if c.CostCoins > 0 {
|
||||
return int(c.CostCoins)
|
||||
}
|
||||
return c.SingleCoins
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/upstream"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const chapterCollectListPath = "/api/app/playlet/chapter/collect/list"
|
||||
|
||||
// PlayletChapterCollectData 对应 swagger proto.PlayletChapterCollectData
|
||||
type PlayletChapterCollectData struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
ChapterTitle string `json:"chapterTitle"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
PlayletSlug string `json:"playletSlug"`
|
||||
PlayletTitle string `json:"playletTitle"`
|
||||
PlayletCover string `json:"playletCover"`
|
||||
CurrentEpisode int `json:"currentEpisode"`
|
||||
TotalEpisode int `json:"totalEpisode"`
|
||||
UpdateStatus int `json:"updateStatus"`
|
||||
CountBrowse int64 `json:"countBrowse"`
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
CountLike int64 `json:"countLike"`
|
||||
IsCollect int `json:"isCollect"`
|
||||
CollectedAt string `json:"collectedAt"`
|
||||
MediaTime int `json:"mediaTime"`
|
||||
PayType int `json:"payType"`
|
||||
SingleCoins int `json:"singleCoins"`
|
||||
VideoURL string `json:"videoUrl"`
|
||||
}
|
||||
|
||||
// PlayletChapterCollectListResp 对应 swagger proto.PlayletChapterCollectListResp
|
||||
type PlayletChapterCollectListResp struct {
|
||||
List []PlayletChapterCollectData `json:"list"`
|
||||
PageNum int `json:"pageNum"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
}
|
||||
|
||||
// FetchChapterCollectList 拉取当前用户剧集收藏列表。
|
||||
// 第二个返回值为上游业务 code(网络错误时为 0)。
|
||||
func FetchChapterCollectList(c *gin.Context, q CollectListQuery) (*PlayletChapterCollectListResp, int, error) {
|
||||
q.Normalize()
|
||||
params := url.Values{}
|
||||
params.Set("pageNum", strconv.Itoa(q.PageNum))
|
||||
params.Set("pageSize", strconv.Itoa(q.PageSize))
|
||||
|
||||
var env APIEnvelope[PlayletChapterCollectListResp]
|
||||
if err := upstream.GetJSON(c, chapterCollectListPath+"?"+params.Encode(), &env); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return nil, env.Code, fmt.Errorf("playlet chapter collect list code=%d msg=%s", env.Code, env.Msg)
|
||||
}
|
||||
return &env.Data, env.Code, nil
|
||||
}
|
||||
|
||||
// FetchChapterCollectListOrEmpty 失败时返回空列表;游客会话失效时尝试重登后重试一次。
|
||||
func FetchChapterCollectListOrEmpty(c *gin.Context, q CollectListQuery) *PlayletChapterCollectListResp {
|
||||
data, code, err := FetchChapterCollectList(c, q)
|
||||
if err == nil {
|
||||
if data == nil {
|
||||
return &PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
return data
|
||||
}
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
data, _, err = FetchChapterCollectList(c, q)
|
||||
if err == nil {
|
||||
if data == nil {
|
||||
return &PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
log.Error("fetch playlet chapter collect list failed", log.E(err))
|
||||
return &PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/upstream"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
chapterLikeSetPath = "/api/app/playlet/chapter/like/set"
|
||||
chapterCollectSetPath = "/api/app/playlet/chapter/collect/set"
|
||||
)
|
||||
|
||||
// PlayletChapterLikeSetReq 对应 swagger proto.PlayletChapterLikeSetReq
|
||||
type PlayletChapterLikeSetReq struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
IsLike bool `json:"isLike"`
|
||||
}
|
||||
|
||||
// PlayletChapterLikeSetResp 对应 swagger proto.PlayletChapterLikeSetResp
|
||||
type PlayletChapterLikeSetResp struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
CountLike int64 `json:"countLike"`
|
||||
IsLike int `json:"isLike"`
|
||||
}
|
||||
|
||||
// ChapterLikeSet 设置剧集点赞/取消点赞。
|
||||
func ChapterLikeSet(c *gin.Context, req PlayletChapterLikeSetReq) (*APIEnvelope[PlayletChapterLikeSetResp], error) {
|
||||
if req.ChapterID <= 0 {
|
||||
return nil, fmt.Errorf("chapter like invalid chapterId")
|
||||
}
|
||||
var env APIEnvelope[PlayletChapterLikeSetResp]
|
||||
if _, err := upstream.PostJSON(c, chapterLikeSetPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// PlayletChapterCollectSetReq 对应 swagger proto.PlayletChapterCollectSetReq
|
||||
type PlayletChapterCollectSetReq struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
IsCollect bool `json:"isCollect"`
|
||||
}
|
||||
|
||||
// PlayletChapterCollectSetResp 对应 swagger proto.PlayletChapterCollectSetResp
|
||||
type PlayletChapterCollectSetResp struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
IsCollect int `json:"isCollect"`
|
||||
}
|
||||
|
||||
// ChapterCollectSet 设置剧集收藏/取消收藏。
|
||||
func ChapterCollectSet(c *gin.Context, req PlayletChapterCollectSetReq) (*APIEnvelope[PlayletChapterCollectSetResp], error) {
|
||||
if req.ChapterID <= 0 {
|
||||
return nil, fmt.Errorf("chapter collect invalid chapterId")
|
||||
}
|
||||
var env APIEnvelope[PlayletChapterCollectSetResp]
|
||||
if _, err := upstream.PostJSON(c, chapterCollectSetPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/upstream"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const chapterUnlockPath = "/api/app/playlet/chapter/unlock"
|
||||
|
||||
// PlayletChapterUnlockReq 对应 swagger proto.PlayletChapterUnlockReq
|
||||
// POST /api/app/playlet/chapter/unlock
|
||||
type PlayletChapterUnlockReq struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
}
|
||||
|
||||
// PlayletChapterUnlockResp 对应 swagger proto.PlayletChapterUnlockResp
|
||||
type PlayletChapterUnlockResp struct {
|
||||
Balance int64 `json:"balance"`
|
||||
CanWatch bool `json:"canWatch"`
|
||||
ChangeCoin int64 `json:"changeCoin"`
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
CostCoins int64 `json:"costCoins"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
}
|
||||
|
||||
// ChapterUnlock 使用金币解锁剧集(幂等)。
|
||||
func ChapterUnlock(c *gin.Context, req PlayletChapterUnlockReq) (*APIEnvelope[PlayletChapterUnlockResp], error) {
|
||||
if req.ChapterID <= 0 || req.PlayletID <= 0 {
|
||||
return nil, fmt.Errorf("chapter unlock invalid chapterId/playletId")
|
||||
}
|
||||
var env APIEnvelope[PlayletChapterUnlockResp]
|
||||
if _, err := upstream.PostJSON(c, chapterUnlockPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/upstream"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const chapterWatchReportPath = "/api/app/playlet/chapter/watch/report"
|
||||
|
||||
// PlayletChapterWatchReportReq 对应 swagger proto.PlayletChapterWatchReportReq
|
||||
type PlayletChapterWatchReportReq struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
PlayletID int64 `json:"playletId,omitempty"`
|
||||
}
|
||||
|
||||
// PlayletChapterWatchReportResp 对应 swagger proto.PlayletChapterWatchReportResp
|
||||
type PlayletChapterWatchReportResp struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
CurrentEpisode int `json:"currentEpisode"`
|
||||
LastChapterID int64 `json:"lastChapterId"`
|
||||
LastEpisode int `json:"lastEpisode"`
|
||||
WatchedCount int `json:"watchedCount"`
|
||||
UnwatchedCount int `json:"unwatchedCount"`
|
||||
}
|
||||
|
||||
// ChapterWatchReport 上报短剧剧集观看。
|
||||
func ChapterWatchReport(c *gin.Context, req PlayletChapterWatchReportReq) (*APIEnvelope[PlayletChapterWatchReportResp], error) {
|
||||
if req.ChapterID <= 0 {
|
||||
return nil, fmt.Errorf("chapter watch report invalid chapterId")
|
||||
}
|
||||
var env APIEnvelope[PlayletChapterWatchReportResp]
|
||||
if _, err := upstream.PostJSON(c, chapterWatchReportPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/upstream"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
collectSetPath = "/api/app/playlet/collect/set"
|
||||
collectListPath = "/api/app/playlet/collect/list"
|
||||
defaultCollectPageSize = 40
|
||||
|
||||
CollectTypePlaylet = "playlet"
|
||||
CollectTypeChapter = "chapter"
|
||||
)
|
||||
|
||||
// PlayletCollectSetReq 对应 swagger proto.PlayletCollectSetReq
|
||||
type PlayletCollectSetReq struct {
|
||||
ID int64 `json:"id"`
|
||||
IsCollect bool `json:"isCollect"`
|
||||
}
|
||||
|
||||
// PlayletCollectSetResp 对应 swagger proto.PlayletCollectSetResp
|
||||
type PlayletCollectSetResp struct {
|
||||
ID int64 `json:"id"`
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
IsCollect int `json:"isCollect"`
|
||||
}
|
||||
|
||||
// PlayletCollectData 对应 swagger proto.PlayletCollectData
|
||||
type PlayletCollectData struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Cover string `json:"cover"`
|
||||
Desc string `json:"desc"`
|
||||
TagIDs []int64 `json:"tagIds"`
|
||||
TagNames []string `json:"tagNames"`
|
||||
TagSlugs []string `json:"tagSlugs"`
|
||||
ThemeIDs []int64 `json:"themeIds"`
|
||||
ThemeNames []string `json:"themeNames"`
|
||||
ThemeSlugs []string `json:"themeSlugs"`
|
||||
TotalEpisode int `json:"totalEpisode"`
|
||||
UpdateStatus int `json:"updateStatus"`
|
||||
CountBrowse int64 `json:"countBrowse"`
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
CountLike int64 `json:"countLike"`
|
||||
IsFeatured bool `json:"isFeatured"`
|
||||
IsNew bool `json:"isNew"`
|
||||
IsRecommend bool `json:"isRecommend"`
|
||||
IsCollect int `json:"isCollect"`
|
||||
CollectedAt string `json:"collectedAt"`
|
||||
WatchedCount int `json:"watchedCount"`
|
||||
UnwatchedCount int `json:"unwatchedCount"`
|
||||
LastChapterID int64 `json:"lastChapterId"`
|
||||
LastEpisode int `json:"lastEpisode"`
|
||||
}
|
||||
|
||||
// PlayletCollectListResp 对应 swagger proto.PlayletCollectListResp
|
||||
type PlayletCollectListResp struct {
|
||||
List []PlayletCollectData `json:"list"`
|
||||
PageNum int `json:"pageNum"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
}
|
||||
|
||||
// CollectListQuery 收藏列表分页参数。
|
||||
type CollectListQuery struct {
|
||||
PageNum int
|
||||
PageSize int
|
||||
Type string // playlet | chapter
|
||||
}
|
||||
|
||||
// NormalizeCollectType 归一化收藏类型。
|
||||
func NormalizeCollectType(raw string) string {
|
||||
switch strings.TrimSpace(strings.ToLower(raw)) {
|
||||
case CollectTypeChapter, "2", "episode", "ep":
|
||||
return CollectTypeChapter
|
||||
default:
|
||||
return CollectTypePlaylet
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize 校正分页默认值。
|
||||
func (q *CollectListQuery) Normalize() {
|
||||
if q.PageNum <= 0 {
|
||||
q.PageNum = 1
|
||||
}
|
||||
if q.PageSize <= 0 {
|
||||
q.PageSize = defaultCollectPageSize
|
||||
}
|
||||
if q.PageSize > 50 {
|
||||
q.PageSize = 50
|
||||
}
|
||||
q.Type = NormalizeCollectType(q.Type)
|
||||
}
|
||||
|
||||
// CollectSet 设置短剧收藏/取消收藏。
|
||||
func CollectSet(c *gin.Context, req PlayletCollectSetReq) (*APIEnvelope[PlayletCollectSetResp], error) {
|
||||
if req.ID <= 0 {
|
||||
return nil, fmt.Errorf("playlet collect invalid id")
|
||||
}
|
||||
var env APIEnvelope[PlayletCollectSetResp]
|
||||
if _, err := upstream.PostJSON(c, collectSetPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// FetchCollectList 拉取当前用户短剧收藏列表。
|
||||
// 第二个返回值为上游业务 code(网络错误时为 0)。
|
||||
func FetchCollectList(c *gin.Context, q CollectListQuery) (*PlayletCollectListResp, int, error) {
|
||||
q.Normalize()
|
||||
params := url.Values{}
|
||||
params.Set("pageNum", strconv.Itoa(q.PageNum))
|
||||
params.Set("pageSize", strconv.Itoa(q.PageSize))
|
||||
|
||||
var env APIEnvelope[PlayletCollectListResp]
|
||||
if err := upstream.GetJSON(c, collectListPath+"?"+params.Encode(), &env); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return nil, env.Code, fmt.Errorf("playlet collect list code=%d msg=%s", env.Code, env.Msg)
|
||||
}
|
||||
return &env.Data, env.Code, nil
|
||||
}
|
||||
|
||||
// FetchCollectListOrEmpty 失败时返回空列表;游客会话失效时尝试重登后重试一次。
|
||||
func FetchCollectListOrEmpty(c *gin.Context, q CollectListQuery) *PlayletCollectListResp {
|
||||
data, code, err := FetchCollectList(c, q)
|
||||
if err == nil {
|
||||
if data == nil {
|
||||
return &PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
return data
|
||||
}
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
data, _, err = FetchCollectList(c, q)
|
||||
if err == nil {
|
||||
if data == nil {
|
||||
return &PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
log.Error("fetch playlet collect list failed", log.E(err))
|
||||
return &PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func collectPageURL(pageNum int, collectType string) string {
|
||||
collectType = NormalizeCollectType(collectType)
|
||||
path := "/collect"
|
||||
if collectType != CollectTypePlaylet {
|
||||
path += "/" + url.PathEscape(collectType)
|
||||
}
|
||||
if pageNum > 1 {
|
||||
path += "/page/" + strconv.Itoa(pageNum)
|
||||
}
|
||||
return path + "/"
|
||||
}
|
||||
|
||||
func CollectCanonicalPath(q CollectListQuery) string {
|
||||
q.Normalize()
|
||||
return collectPageURL(q.PageNum, q.Type)
|
||||
}
|
||||
|
||||
// BuildCollectPagination 收藏列表分页。
|
||||
func BuildCollectPagination(q CollectListQuery, totalCount int64) PaginationVM {
|
||||
q.Normalize()
|
||||
return BuildPagination(func(p int) string {
|
||||
return collectPageURL(p, q.Type)
|
||||
}, q.PageNum, q.PageSize, totalCount)
|
||||
}
|
||||
|
||||
// ParseCollectListQuery 从 query 解析分页与类型(playlet 短剧 / chapter 剧集)。
|
||||
func ParseCollectListQuery(pageStr, typeStr string) CollectListQuery {
|
||||
q := CollectListQuery{
|
||||
PageSize: defaultCollectPageSize,
|
||||
Type: NormalizeCollectType(typeStr),
|
||||
}
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(pageStr)); err == nil {
|
||||
q.PageNum = n
|
||||
}
|
||||
q.Normalize()
|
||||
return q
|
||||
}
|
||||
|
||||
// collectProgressText 进度文案:优先最近观看集,其次未看/已看完。
|
||||
func collectProgressText(item PlayletCollectData) string {
|
||||
if item.LastEpisode > 0 {
|
||||
return fmt.Sprintf("第%d集", item.LastEpisode)
|
||||
}
|
||||
if item.TotalEpisode > 0 && item.UnwatchedCount <= 0 {
|
||||
return "已看完"
|
||||
}
|
||||
if item.UnwatchedCount > 0 {
|
||||
return fmt.Sprintf("%d集未看", item.UnwatchedCount)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// collectTagText 收藏卡片标签:仅展示 TagNames 第一项,无标签时不显示。
|
||||
func collectTagText(item PlayletCollectData) string {
|
||||
return firstName(item.TagNames)
|
||||
}
|
||||
|
||||
// chapterCollectEpisodeLabel 剧集收藏角标:总集数。
|
||||
func chapterCollectEpisodeLabel(item PlayletChapterCollectData) string {
|
||||
return episodeText(item.TotalEpisode)
|
||||
}
|
||||
|
||||
// chapterCollectDesc 剧集收藏副标题。
|
||||
func chapterCollectDesc(item PlayletChapterCollectData) string {
|
||||
if t := strings.TrimSpace(item.ChapterTitle); t != "" {
|
||||
return t
|
||||
}
|
||||
if item.CurrentEpisode > 0 {
|
||||
return fmt.Sprintf("第%d集", item.CurrentEpisode)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// chapterCollectMeta 剧集收藏右侧文案:第 N 集。
|
||||
func chapterCollectMeta(item PlayletChapterCollectData) string {
|
||||
if item.CurrentEpisode > 0 {
|
||||
return fmt.Sprintf("第%d集", item.CurrentEpisode)
|
||||
}
|
||||
return updateStatusText(item.UpdateStatus)
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/upstream"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
historyListPath = "/api/app/playlet/history/list"
|
||||
historyClearPath = "/api/app/playlet/history/clear"
|
||||
defaultHistoryPageSize = 40
|
||||
)
|
||||
|
||||
// PlayletHistoryData 对应 swagger proto.PlayletHistoryData
|
||||
// GET /api/app/playlet/history/list
|
||||
type PlayletHistoryData struct {
|
||||
ID int64 `json:"id"`
|
||||
Status int `json:"status"`
|
||||
IsFeatured bool `json:"isFeatured"`
|
||||
IsNew bool `json:"isNew"`
|
||||
IsRecommend bool `json:"isRecommend"`
|
||||
ShowOnBanner bool `json:"showOnBanner"`
|
||||
Banner string `json:"banner"`
|
||||
OnlineAt string `json:"onlineAt"`
|
||||
MediaCreatedAt string `json:"mediaCreatedAt"`
|
||||
MediaUpdatedAt string `json:"mediaUpdatedAt"`
|
||||
ThemeIDs []int64 `json:"themeIds"`
|
||||
ThemeNames []string `json:"themeNames"`
|
||||
ThemeSlugs []string `json:"themeSlugs"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Desc string `json:"desc"`
|
||||
Anchor string `json:"anchor"`
|
||||
Cover string `json:"cover"`
|
||||
TagIDs []int64 `json:"tagIds"`
|
||||
TagNames []string `json:"tagNames"`
|
||||
TagSlugs []string `json:"tagSlugs"`
|
||||
TotalEpisode int `json:"totalEpisode"`
|
||||
UpdateStatus int `json:"updateStatus"`
|
||||
CountBrowse int64 `json:"countBrowse"`
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
CountLike int64 `json:"countLike"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
WatchedCount int `json:"watchedCount"`
|
||||
UnwatchedCount int `json:"unwatchedCount"`
|
||||
LastChapterID int64 `json:"lastChapterId"`
|
||||
LastEpisode int `json:"lastEpisode"`
|
||||
IsFinished bool `json:"isFinished"`
|
||||
HistoryAt string `json:"historyAt"`
|
||||
}
|
||||
|
||||
// PlayletHistoryListResp 对应 swagger proto.PlayletHistoryListResp
|
||||
type PlayletHistoryListResp struct {
|
||||
List []PlayletHistoryData `json:"list"`
|
||||
PageNum int `json:"pageNum"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
}
|
||||
|
||||
// PlayletHistoryClearResp 对应 swagger proto.PlayletHistoryClearResp
|
||||
type PlayletHistoryClearResp struct {
|
||||
DeletedCount int `json:"deletedCount"`
|
||||
}
|
||||
|
||||
// HistoryListQuery 观看历史分页参数。
|
||||
type HistoryListQuery struct {
|
||||
PageNum int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// Normalize 校正分页默认值。
|
||||
func (q *HistoryListQuery) Normalize() {
|
||||
if q.PageNum <= 0 {
|
||||
q.PageNum = 1
|
||||
}
|
||||
if q.PageSize <= 0 {
|
||||
q.PageSize = defaultHistoryPageSize
|
||||
}
|
||||
if q.PageSize > 100 {
|
||||
q.PageSize = 100
|
||||
}
|
||||
}
|
||||
|
||||
// FetchHistoryList 分页查询当前用户观看历史。
|
||||
// 第二个返回值为上游业务 code(网络错误时为 0)。
|
||||
func FetchHistoryList(c *gin.Context, q HistoryListQuery) (*PlayletHistoryListResp, int, error) {
|
||||
q.Normalize()
|
||||
params := url.Values{}
|
||||
params.Set("pageNum", strconv.Itoa(q.PageNum))
|
||||
params.Set("pageSize", strconv.Itoa(q.PageSize))
|
||||
|
||||
var env APIEnvelope[PlayletHistoryListResp]
|
||||
if err := upstream.GetJSON(c, historyListPath+"?"+params.Encode(), &env); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return nil, env.Code, fmt.Errorf("playlet history list code=%d msg=%s", env.Code, env.Msg)
|
||||
}
|
||||
return &env.Data, env.Code, nil
|
||||
}
|
||||
|
||||
// FetchHistoryListOrEmpty 失败时返回空列表;游客会话失效时尝试重登后重试一次。
|
||||
func FetchHistoryListOrEmpty(c *gin.Context, q HistoryListQuery) *PlayletHistoryListResp {
|
||||
data, code, err := FetchHistoryList(c, q)
|
||||
if err == nil {
|
||||
if data == nil {
|
||||
return &PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
return data
|
||||
}
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
data, _, err = FetchHistoryList(c, q)
|
||||
if err == nil {
|
||||
if data == nil {
|
||||
return &PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
log.Error("fetch playlet history list failed", log.E(err))
|
||||
return &PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
|
||||
// HistoryClear 清空当前用户观看历史。
|
||||
// POST /api/app/playlet/history/clear,无请求体。
|
||||
func HistoryClear(c *gin.Context) (*APIEnvelope[PlayletHistoryClearResp], error) {
|
||||
var env APIEnvelope[PlayletHistoryClearResp]
|
||||
if _, err := upstream.PostJSON(c, historyClearPath, nil, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func historyPageURL(pageNum int) string {
|
||||
if pageNum <= 1 {
|
||||
return "/history/"
|
||||
}
|
||||
return "/history/page/" + strconv.Itoa(pageNum) + "/"
|
||||
}
|
||||
|
||||
func HistoryCanonicalPath(q HistoryListQuery) string {
|
||||
q.Normalize()
|
||||
return historyPageURL(q.PageNum)
|
||||
}
|
||||
|
||||
// BuildHistoryPagination 观看历史分页。
|
||||
func BuildHistoryPagination(q HistoryListQuery, totalCount int64) PaginationVM {
|
||||
q.Normalize()
|
||||
return BuildPagination(func(p int) string {
|
||||
return historyPageURL(p)
|
||||
}, q.PageNum, q.PageSize, totalCount)
|
||||
}
|
||||
|
||||
// ParseHistoryListQuery 从 query 解析分页。
|
||||
func ParseHistoryListQuery(pageStr string) HistoryListQuery {
|
||||
q := HistoryListQuery{PageSize: defaultHistoryPageSize}
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(pageStr)); err == nil {
|
||||
q.PageNum = n
|
||||
}
|
||||
q.Normalize()
|
||||
return q
|
||||
}
|
||||
|
||||
func historyFinished(item PlayletHistoryData) bool {
|
||||
if item.IsFinished {
|
||||
return true
|
||||
}
|
||||
return item.TotalEpisode > 0 && item.UnwatchedCount <= 0 && item.WatchedCount > 0
|
||||
}
|
||||
|
||||
// historyStatus 卡片筛选状态:finished / unfinished。
|
||||
func historyStatus(item PlayletHistoryData) string {
|
||||
if historyFinished(item) {
|
||||
return "finished"
|
||||
}
|
||||
return "unfinished"
|
||||
}
|
||||
|
||||
// historyProgressText PC 卡片未看文案,使用接口 unwatchedCount / isFinished。
|
||||
func historyProgressText(item PlayletHistoryData) string {
|
||||
if historyFinished(item) {
|
||||
return "已看完"
|
||||
}
|
||||
if item.UnwatchedCount > 0 {
|
||||
return fmt.Sprintf("%d集未看", item.UnwatchedCount)
|
||||
}
|
||||
if item.LastEpisode > 0 {
|
||||
return fmt.Sprintf("看到第%d集", item.LastEpisode)
|
||||
}
|
||||
return "未看完"
|
||||
}
|
||||
|
||||
// historyWatchLine H5 进度行。
|
||||
func historyWatchLine(item PlayletHistoryData) string {
|
||||
if historyFinished(item) {
|
||||
if item.LastEpisode > 0 {
|
||||
return fmt.Sprintf("第%d集 · 已看完", item.LastEpisode)
|
||||
}
|
||||
return "已看完"
|
||||
}
|
||||
ep := "未开始"
|
||||
if item.LastEpisode > 0 {
|
||||
ep = fmt.Sprintf("第%d集", item.LastEpisode)
|
||||
}
|
||||
if item.UnwatchedCount > 0 {
|
||||
return ep + fmt.Sprintf(" · 还有%d集未看", item.UnwatchedCount)
|
||||
}
|
||||
return ep + " · 未看完"
|
||||
}
|
||||
|
||||
// historyTagText 历史卡片标签:仅展示 TagNames 第一项,无标签时不显示。
|
||||
func historyTagText(item PlayletHistoryData) string {
|
||||
return firstName(item.TagNames)
|
||||
}
|
||||
|
||||
// historyPlayURL 继续播放:优先最近观看剧集。
|
||||
func historyPlayURL(item PlayletHistoryData) string {
|
||||
return chapterPlayURL(item.Slug, item.ID, item.LastChapterID)
|
||||
}
|
||||
|
||||
// formatHistoryAt 最近观看时间展示。
|
||||
func formatHistoryAt(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
t, ok := parseHistoryTime(raw)
|
||||
if !ok {
|
||||
return raw
|
||||
}
|
||||
now := time.Now()
|
||||
startToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
startThat := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
dayDiff := int(startToday.Sub(startThat).Hours() / 24)
|
||||
hm := t.Format("15:04")
|
||||
switch {
|
||||
case dayDiff <= 0:
|
||||
return "今天 " + hm
|
||||
case dayDiff == 1:
|
||||
return "昨天 " + hm
|
||||
case dayDiff == 2:
|
||||
return "前天 " + hm
|
||||
case dayDiff < 7:
|
||||
return fmt.Sprintf("%d天前 %s", dayDiff, hm)
|
||||
case dayDiff < 14:
|
||||
return "一周前 " + hm
|
||||
default:
|
||||
return t.Format("2006-01-02 15:04")
|
||||
}
|
||||
}
|
||||
|
||||
func parseHistoryTime(raw string) (time.Time, bool) {
|
||||
layouts := []string{
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02T15:04:05Z07:00",
|
||||
"2006-01-02 15:04:05.000",
|
||||
"2006-01-02 15:04:05.999999999",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
|
||||
return t, true
|
||||
}
|
||||
if t, err := time.Parse(layout, raw); err == nil {
|
||||
return t.In(time.Local), true
|
||||
}
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package playletser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/upstream"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const likeSetPath = "/api/app/playlet/like/set"
|
||||
|
||||
// PlayletLikeSetReq 对应 swagger proto.PlayletLikeSetReq
|
||||
type PlayletLikeSetReq struct {
|
||||
ID int64 `json:"id"`
|
||||
IsLike bool `json:"isLike"`
|
||||
}
|
||||
|
||||
// PlayletLikeSetResp 对应 swagger proto.PlayletLikeSetResp
|
||||
type PlayletLikeSetResp struct {
|
||||
ID int64 `json:"id"`
|
||||
CountLike int64 `json:"countLike"`
|
||||
IsLike int `json:"isLike"`
|
||||
}
|
||||
|
||||
// LikeSet 设置短剧点赞/取消点赞。
|
||||
func LikeSet(c *gin.Context, req PlayletLikeSetReq) (*APIEnvelope[PlayletLikeSetResp], error) {
|
||||
if req.ID <= 0 {
|
||||
return nil, fmt.Errorf("playlet like invalid id")
|
||||
}
|
||||
var env APIEnvelope[PlayletLikeSetResp]
|
||||
if _, err := upstream.PostJSON(c, likeSetPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
@@ -11,62 +11,50 @@ import (
|
||||
// TemplateFuncs 模板辅助:仅做展示格式化 / URL,不把接口字段映射成另一套名字。
|
||||
func TemplateFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"detailURL": detailURL,
|
||||
"playURL": playURL,
|
||||
"chapterPlayURL": chapterPlayURL,
|
||||
"categoryURL": categoryURL,
|
||||
"categoryDetailURL": categoryDetailURL,
|
||||
"themeListURL": themeListURLByKey,
|
||||
"coverURL": coverURL,
|
||||
"formatHeat": formatHeat,
|
||||
"formatCollect": formatCollectHeat,
|
||||
"formatLikeCount": formatLikeCount,
|
||||
"episodeLabel": episodeText,
|
||||
"displayTag": displayTag,
|
||||
"firstTagName": firstTagName,
|
||||
"themeTagText": themeTagText,
|
||||
"tagListURL": tagListURL,
|
||||
"cardTag": cardTag,
|
||||
"cardTags": cardTags,
|
||||
"namedTags": namedTags,
|
||||
"namedThemeTags": namedThemeTags,
|
||||
"firstName": firstName,
|
||||
"rankClass": rankClass,
|
||||
"add": addInt,
|
||||
"contains": strings.Contains,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"footerLinkURL": footerLinkURL,
|
||||
"footerLinkIsExternal": footerLinkIsExternal,
|
||||
"footerContactURL": footerContactURL,
|
||||
"footerContactName": footerContactName,
|
||||
"footerContactValue": footerContactValue,
|
||||
"navDisplayURL": navDisplayURL,
|
||||
"urlquery": url.QueryEscape,
|
||||
"pathescape": url.PathEscape,
|
||||
"metaLine": metaLine,
|
||||
"updateStatusText": updateStatusText,
|
||||
"browseText": browseText,
|
||||
"usableAnchor": usableAnchor,
|
||||
"playletYearText": playletYearText,
|
||||
"playletDateText": playletDateText,
|
||||
"isPlayletAllFree": isPlayletAllFree,
|
||||
"collectProgressText": collectProgressText,
|
||||
"collectTagText": collectTagText,
|
||||
"chapterCollectEpisodeLabel": chapterCollectEpisodeLabel,
|
||||
"chapterCollectDesc": chapterCollectDesc,
|
||||
"chapterCollectMeta": chapterCollectMeta,
|
||||
"historyProgressText": historyProgressText,
|
||||
"historyWatchLine": historyWatchLine,
|
||||
"historyTagText": historyTagText,
|
||||
"historyPlayURL": historyPlayURL,
|
||||
"historyStatus": historyStatus,
|
||||
"formatHistoryAt": formatHistoryAt,
|
||||
"formatShortDateTime": formatShortDateTime,
|
||||
"absInt64": absInt64Tpl,
|
||||
"absURL": absURLTpl,
|
||||
"canonicalOr": canonicalOr,
|
||||
"joinStrings": joinStrings,
|
||||
"dict": dict,
|
||||
"detailURL": detailURL,
|
||||
"playURL": playURL,
|
||||
"chapterPlayURL": chapterPlayURL,
|
||||
"categoryURL": categoryURL,
|
||||
"categoryDetailURL": categoryDetailURL,
|
||||
"themeListURL": themeListURLByKey,
|
||||
"coverURL": coverURL,
|
||||
"formatHeat": formatHeat,
|
||||
"formatCollect": formatCollectHeat,
|
||||
"formatLikeCount": formatLikeCount,
|
||||
"episodeLabel": episodeText,
|
||||
"displayTag": displayTag,
|
||||
"firstTagName": firstTagName,
|
||||
"themeTagText": themeTagText,
|
||||
"tagListURL": tagListURL,
|
||||
"cardTag": cardTag,
|
||||
"cardTags": cardTags,
|
||||
"namedTags": namedTags,
|
||||
"namedThemeTags": namedThemeTags,
|
||||
"firstName": firstName,
|
||||
"rankClass": rankClass,
|
||||
"add": addInt,
|
||||
"contains": strings.Contains,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"footerLinkURL": footerLinkURL,
|
||||
"footerLinkIsExternal": footerLinkIsExternal,
|
||||
"footerContactURL": footerContactURL,
|
||||
"footerContactName": footerContactName,
|
||||
"footerContactValue": footerContactValue,
|
||||
"navDisplayURL": navDisplayURL,
|
||||
"urlquery": url.QueryEscape,
|
||||
"pathescape": url.PathEscape,
|
||||
"metaLine": metaLine,
|
||||
"updateStatusText": updateStatusText,
|
||||
"browseText": browseText,
|
||||
"usableAnchor": usableAnchor,
|
||||
"playletYearText": playletYearText,
|
||||
"playletDateText": playletDateText,
|
||||
"isPlayletAllFree": isPlayletAllFree,
|
||||
"absInt64": absInt64Tpl,
|
||||
"absURL": absURLTpl,
|
||||
"canonicalOr": canonicalOr,
|
||||
"joinStrings": joinStrings,
|
||||
"dict": dict,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,25 +303,6 @@ func absInt64Tpl(n int64) int64 {
|
||||
return n
|
||||
}
|
||||
|
||||
func formatShortDateTime(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || strings.HasPrefix(raw, "0001") {
|
||||
return ""
|
||||
}
|
||||
raw = strings.ReplaceAll(raw, "T", " ")
|
||||
if i := strings.Index(raw, "."); i > 0 {
|
||||
raw = raw[:i]
|
||||
}
|
||||
if i := strings.IndexAny(raw, "Z+"); i > 10 {
|
||||
raw = strings.TrimSpace(raw[:i])
|
||||
}
|
||||
if len(raw) >= 16 {
|
||||
raw = raw[:16]
|
||||
}
|
||||
raw = strings.ReplaceAll(raw, "-", "/")
|
||||
return raw
|
||||
}
|
||||
|
||||
func metaLine(item CommonData) string {
|
||||
cat := firstName(item.ThemeNames)
|
||||
if cat == "" {
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/upstream"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
packageListPath = "/api/app/recharge/package/list"
|
||||
coinBuyPath = "/api/app/coin/buy"
|
||||
vipPayPath = "/api/app/vip/pay"
|
||||
vipOrdersPath = "/api/app/vip/orders"
|
||||
coinOrdersPath = "/api/app/coin/orders"
|
||||
coinTransPath = "/api/app/coin/trans/list"
|
||||
)
|
||||
|
||||
// FetchPackageList 拉取购买套餐列表(会员 + 金币)。
|
||||
func FetchPackageList(c *gin.Context) (*RechargePackageListResp, error) {
|
||||
var env APIEnvelope[RechargePackageListResp]
|
||||
if err := upstream.GetJSON(c, packageListPath, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return nil, fmt.Errorf("recharge package list code=%d msg=%s", env.Code, env.Msg)
|
||||
}
|
||||
return &env.Data, nil
|
||||
}
|
||||
|
||||
// FetchPackageListOrEmpty 失败时返回空列表,保证 SSR 可渲染。
|
||||
func FetchPackageListOrEmpty(c *gin.Context) *RechargePackageListResp {
|
||||
data, err := FetchPackageList(c)
|
||||
if err != nil {
|
||||
log.Error("fetch recharge package list failed", log.E(err))
|
||||
return &RechargePackageListResp{}
|
||||
}
|
||||
if data == nil {
|
||||
return &RechargePackageListResp{}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// BuyCoin 购买金币套餐:POST /api/app/coin/buy
|
||||
func BuyCoin(c *gin.Context, req BuyPayReq) (*APIEnvelope[PayOrderResp], error) {
|
||||
var env APIEnvelope[PayOrderResp]
|
||||
if _, err := upstream.PostJSON(c, coinBuyPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// VipPay 购买会员套餐:POST /api/app/vip/pay
|
||||
func VipPay(c *gin.Context, req BuyPayReq) (*APIEnvelope[PayOrderResp], error) {
|
||||
var env APIEnvelope[PayOrderResp]
|
||||
if _, err := upstream.PostJSON(c, vipPayPath, req, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// NormalizePayMode 规范化支付方式。
|
||||
func NormalizePayMode(raw string) string {
|
||||
v := strings.ToLower(strings.TrimSpace(raw))
|
||||
switch v {
|
||||
case "wx", "weixin":
|
||||
return "wechat"
|
||||
case "unionpay":
|
||||
return "union"
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// FetchVipOrders 查询 VIP 充值记录。
|
||||
func FetchVipOrders(c *gin.Context, page, pageSize int) ([]VipOrder, error) {
|
||||
return fetchOrders(c, vipOrdersPath, page, pageSize)
|
||||
}
|
||||
|
||||
// FetchCoinOrders 查询金币购买记录。
|
||||
func FetchCoinOrders(c *gin.Context, page, pageSize int) ([]VipOrder, error) {
|
||||
return fetchOrders(c, coinOrdersPath, page, pageSize)
|
||||
}
|
||||
|
||||
// FetchRecentOrdersOrEmpty 合并 VIP + 金币订单,失败返回空。
|
||||
func FetchRecentOrdersOrEmpty(c *gin.Context, limit int) []VipOrder {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
pageSize := limit
|
||||
vipOrders, err1 := FetchVipOrders(c, 1, pageSize)
|
||||
coinOrders, err2 := FetchCoinOrders(c, 1, pageSize)
|
||||
if err1 != nil {
|
||||
log.Error("fetch vip orders failed", log.E(err1))
|
||||
}
|
||||
if err2 != nil {
|
||||
log.Error("fetch coin orders failed", log.E(err2))
|
||||
}
|
||||
out := make([]VipOrder, 0, len(vipOrders)+len(coinOrders))
|
||||
out = append(out, vipOrders...)
|
||||
out = append(out, coinOrders...)
|
||||
return out
|
||||
}
|
||||
|
||||
// FetchCoinTransList 查询金币余额明细:GET /api/app/coin/trans/list
|
||||
// 第二个返回值为上游业务 code(网络/解码失败时为 0)。
|
||||
func FetchCoinTransList(c *gin.Context, page, pageSize int) ([]CoinTransListItem, int, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
full := fmt.Sprintf("%s?page=%d&pageSize=%d", coinTransPath, page, pageSize)
|
||||
var env APIEnvelopeRaw
|
||||
if err := upstream.GetJSON(c, full, &env); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return nil, env.Code, fmt.Errorf("%s code=%d msg=%s", coinTransPath, env.Code, env.Msg)
|
||||
}
|
||||
return decodeCoinTransList(env.Data), env.Code, nil
|
||||
}
|
||||
|
||||
// FetchCoinTransListOrEmpty 失败返回空列表;游客会话失效时会尝试重登后重试一次。
|
||||
func FetchCoinTransListOrEmpty(c *gin.Context, page, pageSize int) []CoinTransListItem {
|
||||
list, code, err := FetchCoinTransList(c, page, pageSize)
|
||||
if err == nil {
|
||||
return list
|
||||
}
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
list, _, err = FetchCoinTransList(c, page, pageSize)
|
||||
if err == nil {
|
||||
return list
|
||||
}
|
||||
}
|
||||
log.Error("fetch coin trans list failed", log.E(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeCoinTransList(raw json.RawMessage) []CoinTransListItem {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var arr []CoinTransListItem
|
||||
if err := json.Unmarshal(raw, &arr); err == nil {
|
||||
return arr
|
||||
}
|
||||
var obj struct {
|
||||
List []CoinTransListItem `json:"list"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return nil
|
||||
}
|
||||
return obj.List
|
||||
}
|
||||
|
||||
func fetchOrders(c *gin.Context, path string, page, pageSize int) ([]VipOrder, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
full := fmt.Sprintf("%s?page=%d&pageSize=%d", path, page, pageSize)
|
||||
var env APIEnvelopeRaw
|
||||
if err := upstream.GetJSON(c, full, &env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if env.Code != 200 {
|
||||
return nil, fmt.Errorf("%s code=%d msg=%s", path, env.Code, env.Msg)
|
||||
}
|
||||
return decodeOrderList(env.Data), nil
|
||||
}
|
||||
|
||||
func decodeOrderList(raw json.RawMessage) []VipOrder {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var arr []VipOrder
|
||||
if err := json.Unmarshal(raw, &arr); err == nil {
|
||||
return arr
|
||||
}
|
||||
var obj struct {
|
||||
List []VipOrder `json:"list"`
|
||||
Orders []VipOrder `json:"orders"`
|
||||
Records []VipOrder `json:"records"`
|
||||
Items []VipOrder `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case len(obj.List) > 0:
|
||||
return obj.List
|
||||
case len(obj.Orders) > 0:
|
||||
return obj.Orders
|
||||
case len(obj.Records) > 0:
|
||||
return obj.Records
|
||||
case len(obj.Items) > 0:
|
||||
return obj.Items
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package rechargeser
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// PackageType 对应 rechargepackagemod.PackageType
|
||||
const (
|
||||
PackageTypeVip = 1 // 会员畅享
|
||||
PackageTypeCoin = 2 // 金币充值
|
||||
)
|
||||
|
||||
// PayMethodInfo 对应 swagger proto.PayMethodInfo
|
||||
type PayMethodInfo struct {
|
||||
Type string `json:"type"` // alipay / wechat / union
|
||||
TypeName string `json:"typeName"` // 支付宝 / 微信 / 银联
|
||||
}
|
||||
|
||||
// RechargePackageItem 对应 swagger proto.RechargePackageItem
|
||||
type RechargePackageItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SubTitle string `json:"subTitle"`
|
||||
PackageType int `json:"packageType"`
|
||||
Price int64 `json:"price"` // 售价,分
|
||||
OriginalPrice int64 `json:"originalPrice"` // 原价,分
|
||||
BaseCoin int64 `json:"baseCoin"`
|
||||
GiftCoin int64 `json:"giftCoin"`
|
||||
TotalCoin int64 `json:"totalCoin"`
|
||||
DurationDays int `json:"durationDays"`
|
||||
DurationHours int `json:"durationHours"`
|
||||
Sort int `json:"sort"`
|
||||
PayMethods []PayMethodInfo `json:"payMethods"`
|
||||
}
|
||||
|
||||
// RechargePackageListResp 对应 swagger proto.RechargePackageListResp
|
||||
// GET /api/app/recharge/package/list
|
||||
type RechargePackageListResp struct {
|
||||
VipList []RechargePackageItem `json:"vipList"`
|
||||
CoinList []RechargePackageItem `json:"coinList"`
|
||||
}
|
||||
|
||||
// BuyPayReq 对应 coinapi.buyCoinReq / rechargeapi.submitOrderReq
|
||||
type BuyPayReq struct {
|
||||
PackageID int64 `json:"packageId"`
|
||||
PayMode string `json:"payMode"` // alipay / wechat / union ...
|
||||
}
|
||||
|
||||
// PayOrderResp 对应 coinser.BuyCoinResp / rechargeser.SubmitOrderResp
|
||||
type PayOrderResp struct {
|
||||
IsOpenNewBrowser bool `json:"isOpenNewBrowser"`
|
||||
PayURL string `json:"payUrl"`
|
||||
PayURLMode string `json:"payUrlMode"`
|
||||
TradeNo string `json:"tradeNo"`
|
||||
}
|
||||
|
||||
// VipOrder 对应 swagger vipordermod.VipOrder(会员/金币订单共用结构)
|
||||
type VipOrder struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
OrderType int `json:"orderType"` // 1会员 2金币
|
||||
Status int `json:"status"` // 1待支付 2成功 3失败
|
||||
Amount int64 `json:"amount"` // 分
|
||||
Coin int64 `json:"coin"`
|
||||
CardID int64 `json:"cardId"`
|
||||
CardName string `json:"cardName"`
|
||||
CoinCfgID int64 `json:"coinCfgId"`
|
||||
PayMode string `json:"payMode"`
|
||||
TradeNo string `json:"tradeNo"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
FinishedAt string `json:"finishedAt"`
|
||||
}
|
||||
|
||||
// CoinTransListItem 对应 swagger proto.CoinTransListItemResp
|
||||
// GET /api/app/coin/trans/list
|
||||
type CoinTransListItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Remark string `json:"remark"`
|
||||
TranType int `json:"tranType"`
|
||||
TranTypeName string `json:"tranTypeName"`
|
||||
FlowDir int `json:"flowDir"` // 1收入 -1支出
|
||||
FlowDirName string `json:"flowDirName"`
|
||||
Amount int64 `json:"amount"`
|
||||
ChangeAmount int64 `json:"changeAmount"`
|
||||
BalanceBefore int64 `json:"balanceBefore"`
|
||||
BalanceAfter int64 `json:"balanceAfter"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
RelatedID int64 `json:"relatedId"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
const (
|
||||
OrderTypeVip = 1
|
||||
OrderTypeCoin = 2
|
||||
|
||||
OrderStatusPending = 1
|
||||
OrderStatusSuccess = 2
|
||||
OrderStatusFailed = 3
|
||||
|
||||
FlowDirIn = 1
|
||||
FlowDirOut = -1
|
||||
)
|
||||
|
||||
// APIEnvelope 上游统一响应壳
|
||||
type APIEnvelope[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Tip string `json:"tip"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
// APIEnvelopeRaw Data 形态不固定时使用。
|
||||
type APIEnvelopeRaw struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Tip string `json:"tip"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PayMethodVM 支付方式展示。
|
||||
type PayMethodVM struct {
|
||||
Type string
|
||||
TypeName string
|
||||
Active bool
|
||||
}
|
||||
|
||||
// PackageCardVM 充值页套餐卡片(字段来自 package/list)。
|
||||
type PackageCardVM struct {
|
||||
ID int64
|
||||
Name string
|
||||
Hint string // 优先 subTitle
|
||||
PriceText string // 售价(元),不含 ¥
|
||||
OriginalPriceText string // 原价(元),仅当原价>售价时有值
|
||||
PriceFen int64
|
||||
Active bool
|
||||
PayMethods []PayMethodVM
|
||||
PayMethodsJSON string // 供 data-pay-methods 使用
|
||||
}
|
||||
|
||||
// DepositPageVM 充值页模板数据。
|
||||
type DepositPageVM struct {
|
||||
VipList []PackageCardVM
|
||||
CoinList []PackageCardVM
|
||||
PayMethods []PayMethodVM // 当前选中套餐的支付方式(接口 payMethods)
|
||||
// TxList 交易明细:直接透传 /api/app/coin/trans/list 返回的字段
|
||||
//(不再做 Name/TimeText/AmountText 的自定义字段映射)。
|
||||
TxList []CoinTransListItem
|
||||
}
|
||||
|
||||
// TxItemVM 交易明细行(历史遗留:旧逻辑会把 CoinTransListItem 映射到此结构)。
|
||||
// 当前“交易明细调试”需求:DepositPageVM 直接透传 CoinTransListItem,
|
||||
// 但旧函数可能仍被其它页面复用,因此保留结构体定义。
|
||||
type TxItemVM struct {
|
||||
Name string
|
||||
TimeText string
|
||||
AmountText string
|
||||
AmountClass string // deposit-tx-amount--plus / --minus
|
||||
StatusText string
|
||||
}
|
||||
|
||||
var defaultPayMethods = []PayMethodInfo{
|
||||
{Type: "alipay", TypeName: "支付宝"},
|
||||
{Type: "wechat", TypeName: "微信"},
|
||||
{Type: "union", TypeName: "银联"},
|
||||
}
|
||||
|
||||
// BuildDepositPageVM 按接口 vipList / coinList 组装展示数据(支付方式不造假)。
|
||||
// preferTab: "coin" 优先选中金币套餐,其它/空则优先会员。
|
||||
func BuildDepositPageVM(raw *RechargePackageListResp, preferTab string) DepositPageVM {
|
||||
if raw == nil {
|
||||
return DepositPageVM{}
|
||||
}
|
||||
vip := buildCards(raw.VipList, true)
|
||||
coin := buildCards(raw.CoinList, false)
|
||||
activatePreferredPackage(vip, coin, preferTab)
|
||||
|
||||
var activePays []PayMethodVM
|
||||
for _, item := range coin {
|
||||
if item.Active {
|
||||
activePays = item.PayMethods
|
||||
break
|
||||
}
|
||||
}
|
||||
if activePays == nil {
|
||||
for _, item := range vip {
|
||||
if item.Active {
|
||||
activePays = item.PayMethods
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(activePays) > 0 {
|
||||
activePays[0].Active = true
|
||||
}
|
||||
return DepositPageVM{
|
||||
VipList: vip,
|
||||
CoinList: coin,
|
||||
PayMethods: activePays,
|
||||
}
|
||||
}
|
||||
|
||||
// activatePreferredPackage 默认选中:优先有支付方式的套餐,否则选第一项。
|
||||
// preferTab=coin 时优先金币,否则优先会员。
|
||||
func activatePreferredPackage(vip, coin []PackageCardVM, preferTab string) {
|
||||
pick := func(list []PackageCardVM) bool {
|
||||
for i := range list {
|
||||
if len(list[i].PayMethods) > 0 {
|
||||
list[i].Active = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
if len(list) > 0 {
|
||||
list[0].Active = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
preferCoin := strings.EqualFold(strings.TrimSpace(preferTab), "coin")
|
||||
if preferCoin {
|
||||
if pick(coin) {
|
||||
return
|
||||
}
|
||||
pick(vip)
|
||||
return
|
||||
}
|
||||
if pick(vip) {
|
||||
return
|
||||
}
|
||||
pick(coin)
|
||||
}
|
||||
|
||||
// AttachTxList 附加交易明细(金币流水)。
|
||||
func (vm *DepositPageVM) AttachTxList(list []CoinTransListItem) {
|
||||
if vm == nil {
|
||||
return
|
||||
}
|
||||
vm.TxList = list
|
||||
}
|
||||
|
||||
// BuildTxListFromCoinTrans 将金币流水转为展示列表。
|
||||
func BuildTxListFromCoinTrans(list []CoinTransListItem, limit int) []TxItemVM {
|
||||
items := append([]CoinTransListItem(nil), list...)
|
||||
if limit > 0 && len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
out := make([]TxItemVM, 0, len(items))
|
||||
for _, item := range items {
|
||||
name := strings.TrimSpace(item.Title)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(item.TranTypeName)
|
||||
}
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(item.Remark)
|
||||
}
|
||||
if name == "" {
|
||||
name = "金币变动"
|
||||
}
|
||||
change := item.ChangeAmount
|
||||
if change == 0 {
|
||||
change = item.Amount
|
||||
if item.FlowDir == FlowDirOut {
|
||||
change = -change
|
||||
}
|
||||
}
|
||||
amountClass := "deposit-tx-amount--plus"
|
||||
amountText := "+" + formatCount(absInt64(change)) + " 金币"
|
||||
if change < 0 || item.FlowDir == FlowDirOut {
|
||||
amountClass = "deposit-tx-amount--minus"
|
||||
amountText = "-" + formatCount(absInt64(change)) + " 金币"
|
||||
}
|
||||
out = append(out, TxItemVM{
|
||||
Name: name,
|
||||
TimeText: formatTransTime(item.CreatedAt),
|
||||
AmountText: amountText,
|
||||
AmountClass: amountClass,
|
||||
StatusText: strings.TrimSpace(item.FlowDirName),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func absInt64(n int64) int64 {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func formatTransTime(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || strings.HasPrefix(raw, "0001") {
|
||||
return ""
|
||||
}
|
||||
raw = strings.ReplaceAll(raw, "T", " ")
|
||||
if i := strings.Index(raw, "."); i > 0 {
|
||||
raw = raw[:i]
|
||||
}
|
||||
if i := strings.IndexAny(raw, "Z+"); i > 10 {
|
||||
raw = strings.TrimSpace(raw[:i])
|
||||
}
|
||||
if len(raw) > 19 {
|
||||
raw = raw[:19]
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// BuildTxList 将订单转为展示列表(按时间倒序)。保留供订单场景复用。
|
||||
func BuildTxList(orders []VipOrder, limit int) []TxItemVM {
|
||||
items := append([]VipOrder(nil), orders...)
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return orderTimeKey(items[i]) > orderTimeKey(items[j])
|
||||
})
|
||||
if limit > 0 && len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
out := make([]TxItemVM, 0, len(items))
|
||||
for _, o := range items {
|
||||
name := strings.TrimSpace(o.CardName)
|
||||
if name == "" {
|
||||
if o.OrderType == OrderTypeCoin || o.CoinCfgID > 0 {
|
||||
name = "金币充值"
|
||||
} else {
|
||||
name = "会员充值"
|
||||
}
|
||||
}
|
||||
status := orderStatusText(o.Status)
|
||||
if status != "" {
|
||||
name = name + " · " + status
|
||||
}
|
||||
amountClass := "deposit-tx-amount--minus"
|
||||
amountText := "-¥" + FormatYuan(o.Amount)
|
||||
if o.OrderType == OrderTypeCoin || o.Coin > 0 {
|
||||
if o.Status == OrderStatusSuccess && o.Coin > 0 {
|
||||
amountClass = "deposit-tx-amount--plus"
|
||||
amountText = "+" + formatCount(o.Coin) + " 金币"
|
||||
}
|
||||
}
|
||||
out = append(out, TxItemVM{
|
||||
Name: name,
|
||||
TimeText: formatOrderTime(o),
|
||||
AmountText: amountText,
|
||||
AmountClass: amountClass,
|
||||
StatusText: status,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func orderTimeKey(o VipOrder) string {
|
||||
if t := strings.TrimSpace(o.FinishedAt); t != "" && !strings.HasPrefix(t, "0001") {
|
||||
return t
|
||||
}
|
||||
return strings.TrimSpace(o.CreatedAt)
|
||||
}
|
||||
|
||||
func formatOrderTime(o VipOrder) string {
|
||||
raw := orderTimeKey(o)
|
||||
if raw == "" || strings.HasPrefix(raw, "0001") {
|
||||
return ""
|
||||
}
|
||||
// 常见 RFC3339 / 带 T 的时间,截成可读
|
||||
raw = strings.ReplaceAll(raw, "T", " ")
|
||||
if i := strings.Index(raw, "."); i > 0 {
|
||||
raw = raw[:i]
|
||||
}
|
||||
if i := strings.IndexAny(raw, "Z+"); i > 10 {
|
||||
raw = strings.TrimSpace(raw[:i])
|
||||
}
|
||||
if len(raw) > 19 {
|
||||
raw = raw[:19]
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func orderStatusText(status int) string {
|
||||
switch status {
|
||||
case OrderStatusPending:
|
||||
return "待支付"
|
||||
case OrderStatusSuccess:
|
||||
return "已完成"
|
||||
case OrderStatusFailed:
|
||||
return "失败"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func buildCards(list []RechargePackageItem, isVIP bool) []PackageCardVM {
|
||||
items := append([]RechargePackageItem(nil), list...)
|
||||
// 仅按接口 sort 排序;sort 相同保持接口原顺序
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return items[i].Sort < items[j].Sort
|
||||
})
|
||||
out := make([]PackageCardVM, 0, len(items))
|
||||
for _, item := range items {
|
||||
name := strings.TrimSpace(item.Name)
|
||||
if name == "" {
|
||||
if isVIP {
|
||||
name = "会员套餐"
|
||||
} else {
|
||||
name = "金币套餐"
|
||||
}
|
||||
}
|
||||
pays := buildPayMethodVMs(item.PayMethods, false)
|
||||
card := PackageCardVM{
|
||||
ID: item.ID,
|
||||
Name: name,
|
||||
Hint: packageHint(item, isVIP),
|
||||
PriceText: FormatYuan(item.Price),
|
||||
PriceFen: item.Price,
|
||||
PayMethods: pays,
|
||||
PayMethodsJSON: marshalPayMethodsJSON(pays),
|
||||
}
|
||||
if item.OriginalPrice > item.Price && item.OriginalPrice > 0 {
|
||||
card.OriginalPriceText = FormatYuan(item.OriginalPrice)
|
||||
}
|
||||
out = append(out, card)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildPayMethodVMs(list []PayMethodInfo, withFallback bool) []PayMethodVM {
|
||||
src := list
|
||||
if len(src) == 0 && withFallback {
|
||||
src = defaultPayMethods
|
||||
}
|
||||
out := make([]PayMethodVM, 0, len(src))
|
||||
seen := map[string]struct{}{}
|
||||
for _, item := range src {
|
||||
typ := strings.ToLower(strings.TrimSpace(item.Type))
|
||||
name := strings.TrimSpace(item.TypeName)
|
||||
if typ == "" && name == "" {
|
||||
continue
|
||||
}
|
||||
if typ == "" {
|
||||
typ = strings.ToLower(name)
|
||||
}
|
||||
if name == "" {
|
||||
name = defaultPayTypeName(typ)
|
||||
}
|
||||
if _, ok := seen[typ]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typ] = struct{}{}
|
||||
out = append(out, PayMethodVM{Type: typ, TypeName: name})
|
||||
}
|
||||
if len(out) == 0 && withFallback {
|
||||
return buildPayMethodVMs(defaultPayMethods, false)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func defaultPayTypeName(typ string) string {
|
||||
switch typ {
|
||||
case "alipay":
|
||||
return "支付宝"
|
||||
case "wechat", "wx", "weixin":
|
||||
return "微信"
|
||||
case "union", "unionpay":
|
||||
return "银联"
|
||||
case "dcep", "ecny":
|
||||
return "数字人民币"
|
||||
case "usdt":
|
||||
return "USDT"
|
||||
default:
|
||||
if typ == "" {
|
||||
return "支付"
|
||||
}
|
||||
return typ
|
||||
}
|
||||
}
|
||||
|
||||
func marshalPayMethodsJSON(list []PayMethodVM) string {
|
||||
type row struct {
|
||||
Type string `json:"type"`
|
||||
TypeName string `json:"typeName"`
|
||||
}
|
||||
rows := make([]row, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, row{Type: item.Type, TypeName: item.TypeName})
|
||||
}
|
||||
b, err := json.Marshal(rows)
|
||||
if err != nil || len(b) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func packageHint(item RechargePackageItem, isVIP bool) string {
|
||||
// 优先接口 subTitle
|
||||
if s := strings.TrimSpace(item.SubTitle); s != "" {
|
||||
return s
|
||||
}
|
||||
if isVIP {
|
||||
if item.DurationDays > 0 {
|
||||
return fmt.Sprintf("%d天会员", item.DurationDays)
|
||||
}
|
||||
if item.DurationHours > 0 {
|
||||
return fmt.Sprintf("%d小时会员", item.DurationHours)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
base := item.BaseCoin
|
||||
gift := item.GiftCoin
|
||||
total := item.TotalCoin
|
||||
if total <= 0 {
|
||||
total = base + gift
|
||||
}
|
||||
if gift > 0 && base > 0 {
|
||||
return fmt.Sprintf("基础 %s + 赠送 %s", formatCount(base), formatCount(gift))
|
||||
}
|
||||
if total > 0 {
|
||||
return formatCount(total) + " 金币"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// FormatYuan 分转元展示(整数不带小数)。
|
||||
func FormatYuan(fen int64) string {
|
||||
if fen < 0 {
|
||||
fen = 0
|
||||
}
|
||||
if fen%100 == 0 {
|
||||
return strconv.FormatInt(fen/100, 10)
|
||||
}
|
||||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", float64(fen)/100), "0"), ".")
|
||||
}
|
||||
|
||||
func formatCount(n int64) string {
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
s := strconv.FormatInt(n, 10)
|
||||
if len(s) <= 3 {
|
||||
return s
|
||||
}
|
||||
var b strings.Builder
|
||||
rem := len(s) % 3
|
||||
if rem == 0 {
|
||||
rem = 3
|
||||
}
|
||||
b.WriteString(s[:rem])
|
||||
for i := rem; i < len(s); i += 3 {
|
||||
b.WriteByte(',')
|
||||
b.WriteString(s[i : i+3])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -280,8 +280,6 @@ func navActiveKey(href, name string, download bool) string {
|
||||
return "aiSeries"
|
||||
case "/home-road":
|
||||
return "homeRoad"
|
||||
case "/user-center":
|
||||
return "userCenter"
|
||||
case "/download-app":
|
||||
return "downloadApp"
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user