礼物布局
This commit is contained in:
parent
5d2c39f8db
commit
7f16956b7d
@ -95,6 +95,10 @@ func (h *Handler) CreateResource(c *gin.Context) {
|
|||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := validateResourceMetadata(req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
resp, err := h.wallet.CreateResource(c.Request.Context(), req.createProto(c))
|
resp, err := h.wallet.CreateResource(c.Request.Context(), req.createProto(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
@ -203,6 +207,10 @@ func (h *Handler) UpdateResource(c *gin.Context) {
|
|||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := validateResourceMetadata(req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
protoReq := req.updateProto(c, resourceID)
|
protoReq := req.updateProto(c, resourceID)
|
||||||
resp, err := h.wallet.UpdateResource(c.Request.Context(), protoReq)
|
resp, err := h.wallet.UpdateResource(c.Request.Context(), protoReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -150,6 +151,131 @@ func TestProfileCardResourceMetadataKeepsLayoutOnly(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResourceMetadataKeepsMP4AlphaLayoutForAnimatedResources(t *testing.T) {
|
||||||
|
for _, resourceType := range []string{"gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", "mic_seat_animation"} {
|
||||||
|
metadata := resourceMetadataJSON(resourceType, "", mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true), "", "")
|
||||||
|
payload := resourceMetadataPayload{}
|
||||||
|
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||||
|
t.Fatalf("%s mp4 metadata json mismatch: %v", resourceType, err)
|
||||||
|
}
|
||||||
|
if payload.AnimationFormat != resourceAnimationFormatMP4 || payload.MP4AlphaLayout == nil {
|
||||||
|
t.Fatalf("%s mp4 metadata should be kept: %s", resourceType, metadata)
|
||||||
|
}
|
||||||
|
if payload.MP4AlphaLayout.AlphaLayout != mp4AlphaLayoutAlphaLeftRGBRight || payload.MP4AlphaLayout.RGBFrame[0] != 750 {
|
||||||
|
t.Fatalf("%s mp4 layout mismatch: %+v", resourceType, payload.MP4AlphaLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceMetadataKeepsNormalMP4Layout(t *testing.T) {
|
||||||
|
metadata := resourceMetadataJSON("gift", "", `{
|
||||||
|
"animation_format": "mp4",
|
||||||
|
"mp4_alpha_layout": {
|
||||||
|
"alpha_layout": "normal",
|
||||||
|
"video_w": 750,
|
||||||
|
"video_h": 1334,
|
||||||
|
"rgb_frame": [1, 2, 3, 4],
|
||||||
|
"alpha_frame": null,
|
||||||
|
"confirmed": true,
|
||||||
|
"detect_version": 1
|
||||||
|
}
|
||||||
|
}`, "", "")
|
||||||
|
payload := resourceMetadataPayload{}
|
||||||
|
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||||
|
t.Fatalf("normal mp4 metadata json mismatch: %v", err)
|
||||||
|
}
|
||||||
|
if payload.MP4AlphaLayout == nil || payload.MP4AlphaLayout.AlphaLayout != mp4AlphaLayoutNormal {
|
||||||
|
t.Fatalf("normal mp4 metadata should be kept: %s", metadata)
|
||||||
|
}
|
||||||
|
if payload.MP4AlphaLayout.AlphaFrame != nil || payload.MP4AlphaLayout.RGBFrame[2] != 750 || payload.MP4AlphaLayout.RGBFrame[3] != 1334 {
|
||||||
|
t.Fatalf("normal mp4 layout should be normalized to full frame: %+v", payload.MP4AlphaLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProfileCardResourceMetadataKeepsLayoutAndMP4AlphaLayout(t *testing.T) {
|
||||||
|
metadata := resourceMetadataJSON(resourceTypeProfileCard, "", `{
|
||||||
|
"profile_card_layout": {
|
||||||
|
"source_width": 1136,
|
||||||
|
"source_height": 1680,
|
||||||
|
"color_content_width": 750,
|
||||||
|
"content_top": 117,
|
||||||
|
"content_bottom": 1666,
|
||||||
|
"content_height": 1550,
|
||||||
|
"content_top_ratio": 0,
|
||||||
|
"content_height_ratio": 0,
|
||||||
|
"detect_version": 99
|
||||||
|
},
|
||||||
|
"mp4_alpha_layout": {
|
||||||
|
"alpha_layout": "rgb_left_alpha_right",
|
||||||
|
"video_w": 1472,
|
||||||
|
"video_h": 1328,
|
||||||
|
"rgb_frame": [0, 0, 736, 1328],
|
||||||
|
"alpha_frame": [736, 0, 736, 1328],
|
||||||
|
"confirmed": true,
|
||||||
|
"detect_version": 1
|
||||||
|
}
|
||||||
|
}`, "", "")
|
||||||
|
payload := resourceMetadataPayload{}
|
||||||
|
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||||
|
t.Fatalf("profile card mp4 metadata json mismatch: %v", err)
|
||||||
|
}
|
||||||
|
if payload.ProfileCardLayout == nil || payload.ProfileCardLayout.ContentTop != 117 {
|
||||||
|
t.Fatalf("profile card layout should be kept: %+v", payload.ProfileCardLayout)
|
||||||
|
}
|
||||||
|
if payload.MP4AlphaLayout == nil || payload.MP4AlphaLayout.AlphaLayout != mp4AlphaLayoutRGBLeftAlphaRight {
|
||||||
|
t.Fatalf("profile card mp4 layout should be kept: %+v", payload.MP4AlphaLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateResourceMetadataRejectsInvalidMP4AlphaLayout(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
req resourceRequest
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unconfirmed",
|
||||||
|
req: resourceRequest{
|
||||||
|
ResourceType: "gift",
|
||||||
|
MetadataJSON: mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad rect",
|
||||||
|
req: resourceRequest{
|
||||||
|
ResourceType: "gift",
|
||||||
|
MetadataJSON: `{
|
||||||
|
"mp4_alpha_layout": {
|
||||||
|
"alpha_layout": "alpha_left_rgb_right",
|
||||||
|
"video_w": 1500,
|
||||||
|
"video_h": 1334,
|
||||||
|
"rgb_frame": [750, 0, 800, 1334],
|
||||||
|
"alpha_frame": [0, 0, 750, 1334],
|
||||||
|
"confirmed": true,
|
||||||
|
"detect_version": 1
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported type",
|
||||||
|
req: resourceRequest{
|
||||||
|
ResourceType: resourceTypeBadge,
|
||||||
|
MetadataJSON: mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if err := validateResourceMetadata(tc.req); err == nil {
|
||||||
|
t.Fatalf("invalid mp4 metadata should be rejected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := validateResourceMetadata(resourceRequest{ResourceType: "gift", MetadataJSON: mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true)}); err != nil {
|
||||||
|
t.Fatalf("valid mp4 metadata rejected: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProfileCardResourceMetadataRejectsInvalidLayout(t *testing.T) {
|
func TestProfileCardResourceMetadataRejectsInvalidLayout(t *testing.T) {
|
||||||
metadata := resourceMetadataJSON(resourceTypeProfileCard, "", `{
|
metadata := resourceMetadataJSON(resourceTypeProfileCard, "", `{
|
||||||
"profile_card_layout": {
|
"profile_card_layout": {
|
||||||
@ -182,3 +308,19 @@ func TestValidateBadgeResourceRequiresBadgeForm(t *testing.T) {
|
|||||||
t.Fatalf("non-badge resource should not require badge form: %v", err)
|
t.Fatalf("non-badge resource should not require badge form: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mp4AlphaLayoutMetadataJSON(alphaLayout string, confirmed bool) string {
|
||||||
|
return `{
|
||||||
|
"animation_format": "mp4",
|
||||||
|
"mp4_alpha_layout": {
|
||||||
|
"alpha_layout": "` + alphaLayout + `",
|
||||||
|
"video_w": 1500,
|
||||||
|
"video_h": 1334,
|
||||||
|
"rgb_frame": [750, 0, 750, 1334],
|
||||||
|
"alpha_frame": [0, 0, 750, 1334],
|
||||||
|
"confirmed": ` + strconv.FormatBool(confirmed) + `,
|
||||||
|
"detect_version": 1
|
||||||
|
},
|
||||||
|
"debug": true
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|||||||
@ -24,6 +24,20 @@ const (
|
|||||||
resourceTypeProfileCard = "profile_card"
|
resourceTypeProfileCard = "profile_card"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
resourceAnimationFormatMP4 = "mp4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mp4AlphaLayoutNormal = "normal"
|
||||||
|
mp4AlphaLayoutAlphaLeftRGBRight = "alpha_left_rgb_right"
|
||||||
|
mp4AlphaLayoutRGBLeftAlphaRight = "rgb_left_alpha_right"
|
||||||
|
mp4AlphaLayoutAlphaTopRGBBottom = "alpha_top_rgb_bottom"
|
||||||
|
mp4AlphaLayoutRGBTopAlphaBottom = "rgb_top_alpha_bottom"
|
||||||
|
mp4AlphaLayoutCustom = "custom"
|
||||||
|
mp4AlphaLayoutDetectVersion int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
resourcePriceTypeCoin = "coin"
|
resourcePriceTypeCoin = "coin"
|
||||||
resourcePriceTypeFree = "free"
|
resourcePriceTypeFree = "free"
|
||||||
@ -88,6 +102,23 @@ type badgeMetadataPayload struct {
|
|||||||
DefaultSlot string `json:"default_slot,omitempty"`
|
DefaultSlot string `json:"default_slot,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resourceMetadataPayload struct {
|
||||||
|
AnimationFormat string `json:"animation_format,omitempty"`
|
||||||
|
MP4AlphaLayout *mp4AlphaLayoutMetadataPayload `json:"mp4_alpha_layout,omitempty"`
|
||||||
|
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mp4AlphaLayoutMetadataPayload struct {
|
||||||
|
AlphaLayout string `json:"alpha_layout"`
|
||||||
|
VideoWidth int64 `json:"video_w"`
|
||||||
|
VideoHeight int64 `json:"video_h"`
|
||||||
|
RGBFrame []int64 `json:"rgb_frame"`
|
||||||
|
AlphaFrame []int64 `json:"alpha_frame"`
|
||||||
|
Confirmed bool `json:"confirmed"`
|
||||||
|
Confidence float64 `json:"confidence,omitempty"`
|
||||||
|
DetectVersion int64 `json:"detect_version"`
|
||||||
|
}
|
||||||
|
|
||||||
type profileCardMetadataPayload struct {
|
type profileCardMetadataPayload struct {
|
||||||
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
||||||
}
|
}
|
||||||
@ -559,6 +590,39 @@ func validateResourceBadgeForm(req resourceRequest) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateResourceMetadata(req resourceRequest) error {
|
||||||
|
resourceType := normalizeResourceType(req.ResourceType)
|
||||||
|
payload, fields, err := parseResourceMetadataPayload(req.MetadataJSON)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("资源元数据格式不正确")
|
||||||
|
}
|
||||||
|
if payload == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if rawFormat, ok := fields["animation_format"]; ok && len(rawFormat) > 0 {
|
||||||
|
animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat))
|
||||||
|
if animationFormat != "" && animationFormat != resourceAnimationFormatMP4 {
|
||||||
|
return fmt.Errorf("资源动效格式不支持")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := fields["mp4_alpha_layout"]; !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !resourceAllowsAnimationMetadata(resourceType) {
|
||||||
|
return fmt.Errorf("当前资源类型不支持 MP4 透明布局")
|
||||||
|
}
|
||||||
|
layout, err := sanitizeMP4AlphaLayoutMetadata(payload.MP4AlphaLayout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// MP4 透明布局是客户端合成协议,后台只能保存人工确认后的显式矩形,
|
||||||
|
// 否则 Flutter 会重新落回不稳定的自动识别路径,黑场或弱 mask 素材仍会展示错误。
|
||||||
|
if !layout.Confirmed {
|
||||||
|
return fmt.Errorf("请确认 MP4 透明布局")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func resourceGrantStrategy(resourceType string) string {
|
func resourceGrantStrategy(resourceType string) string {
|
||||||
switch resourceType {
|
switch resourceType {
|
||||||
case resourceTypeCoin:
|
case resourceTypeCoin:
|
||||||
@ -573,14 +637,25 @@ func resourceGrantStrategy(resourceType string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string) string {
|
func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string) string {
|
||||||
switch resourceType {
|
if resourceType == resourceTypeBadge {
|
||||||
case resourceTypeBadge:
|
|
||||||
return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
|
return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
|
||||||
case resourceTypeProfileCard:
|
}
|
||||||
return profileCardMetadataJSON(metadataJSON)
|
|
||||||
default:
|
rawPayload, _, err := parseResourceMetadataPayload(metadataJSON)
|
||||||
|
if err != nil || rawPayload == nil {
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
|
payload := resourceMetadataPayload{}
|
||||||
|
if resourceType == resourceTypeProfileCard {
|
||||||
|
payload.ProfileCardLayout = sanitizeProfileCardLayoutMetadata(rawPayload.ProfileCardLayout)
|
||||||
|
}
|
||||||
|
if resourceAllowsAnimationMetadata(resourceType) {
|
||||||
|
if layout, err := sanitizeMP4AlphaLayoutMetadata(rawPayload.MP4AlphaLayout); err == nil && layout != nil && layout.Confirmed {
|
||||||
|
payload.AnimationFormat = resourceAnimationFormatMP4
|
||||||
|
payload.MP4AlphaLayout = layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return marshalResourceMetadataPayload(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func badgeMetadataJSON(badgeForm string, badgeKind string, levelTrack string) string {
|
func badgeMetadataJSON(badgeForm string, badgeKind string, levelTrack string) string {
|
||||||
@ -611,7 +686,7 @@ func profileCardMetadataJSON(metadataJSON string) string {
|
|||||||
if raw == "" {
|
if raw == "" {
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
payload := profileCardMetadataPayload{}
|
payload := resourceMetadataPayload{}
|
||||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
@ -627,6 +702,128 @@ func profileCardMetadataJSON(metadataJSON string) string {
|
|||||||
return string(body)
|
return string(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseResourceMetadataPayload(metadataJSON string) (*resourceMetadataPayload, map[string]json.RawMessage, error) {
|
||||||
|
raw := strings.TrimSpace(metadataJSON)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
fields := map[string]json.RawMessage{}
|
||||||
|
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
payload := resourceMetadataPayload{}
|
||||||
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return &payload, fields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalResourceMetadataPayload(payload resourceMetadataPayload) string {
|
||||||
|
if payload.AnimationFormat == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return string(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeMP4AlphaLayoutMetadata(layout *mp4AlphaLayoutMetadataPayload) (*mp4AlphaLayoutMetadataPayload, error) {
|
||||||
|
if layout == nil {
|
||||||
|
return nil, fmt.Errorf("MP4 透明布局参数不完整")
|
||||||
|
}
|
||||||
|
videoWidth := layout.VideoWidth
|
||||||
|
videoHeight := layout.VideoHeight
|
||||||
|
if videoWidth <= 0 || videoHeight <= 0 {
|
||||||
|
return nil, fmt.Errorf("MP4 视频宽高不正确")
|
||||||
|
}
|
||||||
|
alphaLayout := normalizeMP4AlphaLayout(layout.AlphaLayout)
|
||||||
|
if alphaLayout == "" {
|
||||||
|
return nil, fmt.Errorf("MP4 透明布局类型不支持")
|
||||||
|
}
|
||||||
|
detectVersion := layout.DetectVersion
|
||||||
|
if detectVersion <= 0 {
|
||||||
|
detectVersion = mp4AlphaLayoutDetectVersion
|
||||||
|
}
|
||||||
|
confidence := layout.Confidence
|
||||||
|
if confidence < 0 {
|
||||||
|
confidence = 0
|
||||||
|
}
|
||||||
|
if confidence > 1 {
|
||||||
|
confidence = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if alphaLayout == mp4AlphaLayoutNormal {
|
||||||
|
// normal 也是显式布局:RGB 使用完整视频帧,alpha_frame 固定为 null,
|
||||||
|
// 这样 native 层能明确跳过 split-alpha 猜测,普通 MP4 不再被误判。
|
||||||
|
return &mp4AlphaLayoutMetadataPayload{
|
||||||
|
AlphaLayout: mp4AlphaLayoutNormal,
|
||||||
|
VideoWidth: videoWidth,
|
||||||
|
VideoHeight: videoHeight,
|
||||||
|
RGBFrame: []int64{0, 0, videoWidth, videoHeight},
|
||||||
|
AlphaFrame: nil,
|
||||||
|
Confirmed: layout.Confirmed,
|
||||||
|
Confidence: confidence,
|
||||||
|
DetectVersion: detectVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if !validMP4Frame(layout.RGBFrame, videoWidth, videoHeight) || !validMP4Frame(layout.AlphaFrame, videoWidth, videoHeight) {
|
||||||
|
return nil, fmt.Errorf("MP4 透明布局矩形不正确")
|
||||||
|
}
|
||||||
|
// 透明 MP4 的 RGB 与 alpha 矩形直接影响客户端 shader 取样,后端只做边界归一化,
|
||||||
|
// 不把字段重算成左右或上下默认值,避免覆盖前端人工确认过的 custom 细节。
|
||||||
|
return &mp4AlphaLayoutMetadataPayload{
|
||||||
|
AlphaLayout: alphaLayout,
|
||||||
|
VideoWidth: videoWidth,
|
||||||
|
VideoHeight: videoHeight,
|
||||||
|
RGBFrame: append([]int64(nil), layout.RGBFrame[:4]...),
|
||||||
|
AlphaFrame: append([]int64(nil), layout.AlphaFrame[:4]...),
|
||||||
|
Confirmed: layout.Confirmed,
|
||||||
|
Confidence: confidence,
|
||||||
|
DetectVersion: detectVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validMP4Frame(frame []int64, videoWidth int64, videoHeight int64) bool {
|
||||||
|
if len(frame) < 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
left, top, width, height := frame[0], frame[1], frame[2], frame[3]
|
||||||
|
if left < 0 || top < 0 || width <= 0 || height <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return left+width <= videoWidth && top+height <= videoHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMP4AlphaLayout(value string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
|
case mp4AlphaLayoutNormal:
|
||||||
|
return mp4AlphaLayoutNormal
|
||||||
|
case mp4AlphaLayoutAlphaLeftRGBRight:
|
||||||
|
return mp4AlphaLayoutAlphaLeftRGBRight
|
||||||
|
case mp4AlphaLayoutRGBLeftAlphaRight:
|
||||||
|
return mp4AlphaLayoutRGBLeftAlphaRight
|
||||||
|
case mp4AlphaLayoutAlphaTopRGBBottom:
|
||||||
|
return mp4AlphaLayoutAlphaTopRGBBottom
|
||||||
|
case mp4AlphaLayoutRGBTopAlphaBottom:
|
||||||
|
return mp4AlphaLayoutRGBTopAlphaBottom
|
||||||
|
case mp4AlphaLayoutCustom:
|
||||||
|
return mp4AlphaLayoutCustom
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceAllowsAnimationMetadata(resourceType string) bool {
|
||||||
|
switch resourceType {
|
||||||
|
case "gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return strings.HasPrefix(resourceType, "mic_seat_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload {
|
func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload {
|
||||||
if layout == nil {
|
if layout == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
package mysql_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hyapp/pkg/appcode"
|
|
||||||
"hyapp/pkg/xerr"
|
|
||||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
|
||||||
"hyapp/services/wallet-service/internal/testutil/mysqltest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDebitCPBreakupFeeDebitsCoinAndReplaysIdempotently(t *testing.T) {
|
|
||||||
repo := mysqltest.NewRepository(t)
|
|
||||||
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
||||||
repo.SetBalance(7001, 1000)
|
|
||||||
|
|
||||||
command := ledger.CPBreakupFeeCommand{
|
|
||||||
AppCode: appcode.Default,
|
|
||||||
CommandID: "cp_break_fee_1",
|
|
||||||
UserID: 7001,
|
|
||||||
RelationshipID: "cp_rel:7001:7002",
|
|
||||||
RelationType: "cp",
|
|
||||||
Amount: 88,
|
|
||||||
}
|
|
||||||
first, err := repo.DebitCPBreakupFee(ctx, command)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("debit cp breakup fee failed: %v", err)
|
|
||||||
}
|
|
||||||
if first.CoinSpent != 88 || first.CoinBalanceAfter != 912 || first.Balance.AvailableAmount != 912 {
|
|
||||||
t.Fatalf("receipt mismatch: %+v", first)
|
|
||||||
}
|
|
||||||
if repo.CountRows("wallet_transactions", "command_id = ?", command.CommandID) != 1 {
|
|
||||||
t.Fatalf("expected one wallet transaction")
|
|
||||||
}
|
|
||||||
if repo.CountRows("wallet_entries", "transaction_id = ?", first.TransactionID) != 1 {
|
|
||||||
t.Fatalf("expected one wallet entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
second, err := repo.DebitCPBreakupFee(ctx, command)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("debit cp breakup fee replay failed: %v", err)
|
|
||||||
}
|
|
||||||
if second.TransactionID != first.TransactionID || second.CoinBalanceAfter != first.CoinBalanceAfter {
|
|
||||||
t.Fatalf("idempotent replay mismatch: first=%+v second=%+v", first, second)
|
|
||||||
}
|
|
||||||
if repo.CountRows("wallet_entries", "transaction_id = ?", first.TransactionID) != 1 {
|
|
||||||
t.Fatalf("idempotent replay must not create another entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
command.Amount = 89
|
|
||||||
if _, err := repo.DebitCPBreakupFee(ctx, command); xerr.CodeOf(err) != xerr.LedgerConflict {
|
|
||||||
t.Fatalf("same command with different amount must conflict, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDebitCPBreakupFeeRejectsInsufficientBalance(t *testing.T) {
|
|
||||||
repo := mysqltest.NewRepository(t)
|
|
||||||
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
||||||
repo.SetBalance(7101, 10)
|
|
||||||
|
|
||||||
_, err := repo.DebitCPBreakupFee(ctx, ledger.CPBreakupFeeCommand{
|
|
||||||
AppCode: appcode.Default,
|
|
||||||
CommandID: "cp_break_fee_insufficient",
|
|
||||||
UserID: 7101,
|
|
||||||
RelationshipID: "cp_rel:7101:7102",
|
|
||||||
RelationType: "sister",
|
|
||||||
Amount: 20,
|
|
||||||
})
|
|
||||||
if xerr.CodeOf(err) != xerr.InsufficientBalance {
|
|
||||||
t.Fatalf("insufficient balance must be rejected, got %v", err)
|
|
||||||
}
|
|
||||||
if repo.CountRows("wallet_transactions", "command_id = ?", "cp_break_fee_insufficient") != 0 {
|
|
||||||
t.Fatalf("insufficient balance must not create transaction")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user