主播钻石收入
This commit is contained in:
parent
b24cc5a336
commit
46e280c8a8
@ -6532,8 +6532,10 @@ type SendGiftRequest struct {
|
|||||||
TargetHostRegionId int64 `protobuf:"varint,9,opt,name=target_host_region_id,json=targetHostRegionId,proto3" json:"target_host_region_id,omitempty"`
|
TargetHostRegionId int64 `protobuf:"varint,9,opt,name=target_host_region_id,json=targetHostRegionId,proto3" json:"target_host_region_id,omitempty"`
|
||||||
// target_agency_owner_user_id 是送礼瞬间主播上级代理收款用户;为空表示当前无代理或代理关系不可结算。
|
// target_agency_owner_user_id 是送礼瞬间主播上级代理收款用户;为空表示当前无代理或代理关系不可结算。
|
||||||
TargetAgencyOwnerUserId int64 `protobuf:"varint,10,opt,name=target_agency_owner_user_id,json=targetAgencyOwnerUserId,proto3" json:"target_agency_owner_user_id,omitempty"`
|
TargetAgencyOwnerUserId int64 `protobuf:"varint,10,opt,name=target_agency_owner_user_id,json=targetAgencyOwnerUserId,proto3" json:"target_agency_owner_user_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// sender_region_id 是 gateway 从 user-service 当前送礼用户资料注入的区域,用于 wallet 匹配礼物钻石比例。
|
||||||
sizeCache protoimpl.SizeCache
|
SenderRegionId int64 `protobuf:"varint,11,opt,name=sender_region_id,json=senderRegionId,proto3" json:"sender_region_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SendGiftRequest) Reset() {
|
func (x *SendGiftRequest) Reset() {
|
||||||
@ -6636,6 +6638,13 @@ func (x *SendGiftRequest) GetTargetAgencyOwnerUserId() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *SendGiftRequest) GetSenderRegionId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.SenderRegionId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// SendGiftResponse 返回扣费成功并落到房间后的状态结果。
|
// SendGiftResponse 返回扣费成功并落到房间后的状态结果。
|
||||||
type SendGiftResponse struct {
|
type SendGiftResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@ -9390,7 +9399,7 @@ const file_proto_room_v1_room_proto_rawDesc = "" +
|
|||||||
"\aroom_id\x18\x04 \x01(\tR\x06roomId\x12\x1d\n" +
|
"\aroom_id\x18\x04 \x01(\tR\x06roomId\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"rtc_kicked\x18\x05 \x01(\bR\trtcKicked\x12$\n" +
|
"rtc_kicked\x18\x05 \x01(\bR\trtcKicked\x12$\n" +
|
||||||
"\x0ertc_kick_error\x18\x06 \x01(\tR\frtcKickError\"\x98\x03\n" +
|
"\x0ertc_kick_error\x18\x06 \x01(\tR\frtcKickError\"\xc2\x03\n" +
|
||||||
"\x0fSendGiftRequest\x12.\n" +
|
"\x0fSendGiftRequest\x12.\n" +
|
||||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12$\n" +
|
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12$\n" +
|
||||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x17\n" +
|
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x17\n" +
|
||||||
@ -9404,7 +9413,8 @@ const file_proto_room_v1_room_proto_rawDesc = "" +
|
|||||||
"\x0etarget_is_host\x18\b \x01(\bR\ftargetIsHost\x121\n" +
|
"\x0etarget_is_host\x18\b \x01(\bR\ftargetIsHost\x121\n" +
|
||||||
"\x15target_host_region_id\x18\t \x01(\x03R\x12targetHostRegionId\x12<\n" +
|
"\x15target_host_region_id\x18\t \x01(\x03R\x12targetHostRegionId\x12<\n" +
|
||||||
"\x1btarget_agency_owner_user_id\x18\n" +
|
"\x1btarget_agency_owner_user_id\x18\n" +
|
||||||
" \x01(\x03R\x17targetAgencyOwnerUserId\"\xfb\x02\n" +
|
" \x01(\x03R\x17targetAgencyOwnerUserId\x12(\n" +
|
||||||
|
"\x10sender_region_id\x18\v \x01(\x03R\x0esenderRegionId\"\xfb\x02\n" +
|
||||||
"\x10SendGiftResponse\x124\n" +
|
"\x10SendGiftResponse\x124\n" +
|
||||||
"\x06result\x18\x01 \x01(\v2\x1c.hyapp.room.v1.CommandResultR\x06result\x12,\n" +
|
"\x06result\x18\x01 \x01(\v2\x1c.hyapp.room.v1.CommandResultR\x06result\x12,\n" +
|
||||||
"\x12billing_receipt_id\x18\x02 \x01(\tR\x10billingReceiptId\x12\x1b\n" +
|
"\x12billing_receipt_id\x18\x02 \x01(\tR\x10billingReceiptId\x12\x1b\n" +
|
||||||
|
|||||||
@ -784,6 +784,8 @@ message SendGiftRequest {
|
|||||||
int64 target_host_region_id = 9;
|
int64 target_host_region_id = 9;
|
||||||
// target_agency_owner_user_id 是送礼瞬间主播上级代理收款用户;为空表示当前无代理或代理关系不可结算。
|
// target_agency_owner_user_id 是送礼瞬间主播上级代理收款用户;为空表示当前无代理或代理关系不可结算。
|
||||||
int64 target_agency_owner_user_id = 10;
|
int64 target_agency_owner_user_id = 10;
|
||||||
|
// sender_region_id 是 gateway 从 user-service 当前送礼用户资料注入的区域,用于 wallet 匹配礼物钻石比例。
|
||||||
|
int64 sender_region_id = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendGiftResponse 返回扣费成功并落到房间后的状态结果。
|
// SendGiftResponse 返回扣费成功并落到房间后的状态结果。
|
||||||
|
|||||||
@ -41,8 +41,10 @@ type DebitGiftRequest struct {
|
|||||||
TargetHostRegionId int64 `protobuf:"varint,11,opt,name=target_host_region_id,json=targetHostRegionId,proto3" json:"target_host_region_id,omitempty"`
|
TargetHostRegionId int64 `protobuf:"varint,11,opt,name=target_host_region_id,json=targetHostRegionId,proto3" json:"target_host_region_id,omitempty"`
|
||||||
// target_agency_owner_user_id 是送礼时主播归属 Agency 的 owner,用于后续代理工资结算。
|
// target_agency_owner_user_id 是送礼时主播归属 Agency 的 owner,用于后续代理工资结算。
|
||||||
TargetAgencyOwnerUserId int64 `protobuf:"varint,12,opt,name=target_agency_owner_user_id,json=targetAgencyOwnerUserId,proto3" json:"target_agency_owner_user_id,omitempty"`
|
TargetAgencyOwnerUserId int64 `protobuf:"varint,12,opt,name=target_agency_owner_user_id,json=targetAgencyOwnerUserId,proto3" json:"target_agency_owner_user_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// sender_region_id 是送礼用户所属区域,用于匹配礼物入主播周期钻石比例;不能用房间 visible_region_id 替代。
|
||||||
sizeCache protoimpl.SizeCache
|
SenderRegionId int64 `protobuf:"varint,13,opt,name=sender_region_id,json=senderRegionId,proto3" json:"sender_region_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DebitGiftRequest) Reset() {
|
func (x *DebitGiftRequest) Reset() {
|
||||||
@ -159,6 +161,13 @@ func (x *DebitGiftRequest) GetTargetAgencyOwnerUserId() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DebitGiftRequest) GetSenderRegionId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.SenderRegionId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// DebitGiftResponse 返回扣费流水、服务端结算值和 sender COIN 账后余额。
|
// DebitGiftResponse 返回扣费流水、服务端结算值和 sender COIN 账后余额。
|
||||||
type DebitGiftResponse struct {
|
type DebitGiftResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@ -12519,7 +12528,7 @@ var File_proto_wallet_v1_wallet_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x1cproto/wallet/v1/wallet.proto\x12\x0fhyapp.wallet.v1\"\xc2\x03\n" +
|
"\x1cproto/wallet/v1/wallet.proto\x12\x0fhyapp.wallet.v1\"\xec\x03\n" +
|
||||||
"\x10DebitGiftRequest\x12\x1d\n" +
|
"\x10DebitGiftRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"command_id\x18\x01 \x01(\tR\tcommandId\x12\x17\n" +
|
"command_id\x18\x01 \x01(\tR\tcommandId\x12\x17\n" +
|
||||||
@ -12535,7 +12544,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\x0etarget_is_host\x18\n" +
|
"\x0etarget_is_host\x18\n" +
|
||||||
" \x01(\bR\ftargetIsHost\x121\n" +
|
" \x01(\bR\ftargetIsHost\x121\n" +
|
||||||
"\x15target_host_region_id\x18\v \x01(\x03R\x12targetHostRegionId\x12<\n" +
|
"\x15target_host_region_id\x18\v \x01(\x03R\x12targetHostRegionId\x12<\n" +
|
||||||
"\x1btarget_agency_owner_user_id\x18\f \x01(\x03R\x17targetAgencyOwnerUserId\"\xff\x03\n" +
|
"\x1btarget_agency_owner_user_id\x18\f \x01(\x03R\x17targetAgencyOwnerUserId\x12(\n" +
|
||||||
|
"\x10sender_region_id\x18\r \x01(\x03R\x0esenderRegionId\"\xff\x03\n" +
|
||||||
"\x11DebitGiftResponse\x12,\n" +
|
"\x11DebitGiftResponse\x12,\n" +
|
||||||
"\x12billing_receipt_id\x18\x01 \x01(\tR\x10billingReceiptId\x12%\n" +
|
"\x12billing_receipt_id\x18\x01 \x01(\tR\x10billingReceiptId\x12%\n" +
|
||||||
"\x0etransaction_id\x18\x02 \x01(\tR\rtransactionId\x12\x1d\n" +
|
"\x0etransaction_id\x18\x02 \x01(\tR\rtransactionId\x12\x1d\n" +
|
||||||
|
|||||||
@ -23,6 +23,8 @@ message DebitGiftRequest {
|
|||||||
int64 target_host_region_id = 11;
|
int64 target_host_region_id = 11;
|
||||||
// target_agency_owner_user_id 是送礼时主播归属 Agency 的 owner,用于后续代理工资结算。
|
// target_agency_owner_user_id 是送礼时主播归属 Agency 的 owner,用于后续代理工资结算。
|
||||||
int64 target_agency_owner_user_id = 12;
|
int64 target_agency_owner_user_id = 12;
|
||||||
|
// sender_region_id 是送礼用户所属区域,用于匹配礼物入主播周期钻石比例;不能用房间 visible_region_id 替代。
|
||||||
|
int64 sender_region_id = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebitGiftResponse 返回扣费流水、服务端结算值和 sender COIN 账后余额。
|
// DebitGiftResponse 返回扣费流水、服务端结算值和 sender COIN 账后余额。
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import (
|
|||||||
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
|
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
|
||||||
firstrechargerewardmodule "hyapp-admin-server/internal/modules/firstrechargereward"
|
firstrechargerewardmodule "hyapp-admin-server/internal/modules/firstrechargereward"
|
||||||
gamemanagementmodule "hyapp-admin-server/internal/modules/gamemanagement"
|
gamemanagementmodule "hyapp-admin-server/internal/modules/gamemanagement"
|
||||||
|
giftdiamondmodule "hyapp-admin-server/internal/modules/giftdiamond"
|
||||||
healthmodule "hyapp-admin-server/internal/modules/health"
|
healthmodule "hyapp-admin-server/internal/modules/health"
|
||||||
hostagencypolicymodule "hyapp-admin-server/internal/modules/hostagencypolicy"
|
hostagencypolicymodule "hyapp-admin-server/internal/modules/hostagencypolicy"
|
||||||
hostorgmodule "hyapp-admin-server/internal/modules/hostorg"
|
hostorgmodule "hyapp-admin-server/internal/modules/hostorg"
|
||||||
@ -229,6 +230,7 @@ func main() {
|
|||||||
Dashboard: dashboardmodule.New(store, cfg),
|
Dashboard: dashboardmodule.New(store, cfg),
|
||||||
FirstRechargeReward: firstrechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
FirstRechargeReward: firstrechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
Game: gamemanagementmodule.New(gameclient.NewGRPC(gameConn), auditHandler),
|
Game: gamemanagementmodule.New(gameclient.NewGRPC(gameConn), auditHandler),
|
||||||
|
GiftDiamond: giftdiamondmodule.New(walletDB, auditHandler),
|
||||||
Health: healthmodule.New(store, redisClient, jobStatus),
|
Health: healthmodule.New(store, redisClient, jobStatus),
|
||||||
HostAgencyPolicy: hostagencypolicymodule.New(store, walletDB, auditHandler),
|
HostAgencyPolicy: hostagencypolicymodule.New(store, walletDB, auditHandler),
|
||||||
HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler),
|
HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler),
|
||||||
|
|||||||
@ -37,7 +37,6 @@ type AppUser struct {
|
|||||||
CountryDisplayName string `json:"countryDisplayName"`
|
CountryDisplayName string `json:"countryDisplayName"`
|
||||||
CountryName string `json:"countryName"`
|
CountryName string `json:"countryName"`
|
||||||
CreatedAtMs int64 `json:"createdAtMs"`
|
CreatedAtMs int64 `json:"createdAtMs"`
|
||||||
Diamond int64 `json:"diamond"`
|
|
||||||
DisplayUserID string `json:"displayUserId"`
|
DisplayUserID string `json:"displayUserId"`
|
||||||
Gender string `json:"gender"`
|
Gender string `json:"gender"`
|
||||||
LastActiveAtMs int64 `json:"lastActiveAtMs"`
|
LastActiveAtMs int64 `json:"lastActiveAtMs"`
|
||||||
@ -464,16 +463,16 @@ func (s *Service) fillBalances(ctx context.Context, items []AppUser, userIDs []i
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
placeholders := strings.TrimRight(strings.Repeat("?,", len(userIDs)), ",")
|
placeholders := strings.TrimRight(strings.Repeat("?,", len(userIDs)), ",")
|
||||||
args := make([]any, 0, len(userIDs)+2)
|
args := make([]any, 0, len(userIDs)+1)
|
||||||
args = append(args, appctx.FromContext(ctx))
|
args = append(args, appctx.FromContext(ctx))
|
||||||
for _, id := range userIDs {
|
for _, id := range userIDs {
|
||||||
args = append(args, id)
|
args = append(args, id)
|
||||||
}
|
}
|
||||||
args = append(args, "COIN", "DIAMOND")
|
args = append(args, "COIN")
|
||||||
rows, err := s.walletDB.QueryContext(ctx, fmt.Sprintf(`
|
rows, err := s.walletDB.QueryContext(ctx, fmt.Sprintf(`
|
||||||
SELECT user_id, asset_type, available_amount
|
SELECT user_id, asset_type, available_amount
|
||||||
FROM wallet_accounts
|
FROM wallet_accounts
|
||||||
WHERE app_code = ? AND user_id IN (%s) AND asset_type IN (?, ?)
|
WHERE app_code = ? AND user_id IN (%s) AND asset_type = ?
|
||||||
`, placeholders), args...)
|
`, placeholders), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -498,8 +497,6 @@ func (s *Service) fillBalances(ctx context.Context, items []AppUser, userIDs []i
|
|||||||
switch strings.ToUpper(assetType) {
|
switch strings.ToUpper(assetType) {
|
||||||
case "COIN":
|
case "COIN":
|
||||||
items[i].Coin = amount
|
items[i].Coin = amount
|
||||||
case "DIAMOND":
|
|
||||||
items[i].Diamond = amount
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rows.Err()
|
return rows.Err()
|
||||||
|
|||||||
20
server/admin/internal/modules/giftdiamond/dto.go
Normal file
20
server/admin/internal/modules/giftdiamond/dto.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package giftdiamond
|
||||||
|
|
||||||
|
type ratioDTO struct {
|
||||||
|
GiftTypeCode string `json:"giftTypeCode"`
|
||||||
|
RatioPercent string `json:"ratioPercent"`
|
||||||
|
RegionID int64 `json:"regionId"`
|
||||||
|
EffectiveRegionID int64 `json:"effectiveRegionId"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ratioListDTO struct {
|
||||||
|
RegionID int64 `json:"regionId"`
|
||||||
|
Items []ratioDTO `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateRequest struct {
|
||||||
|
RegionID int64 `json:"regionId"`
|
||||||
|
Ratios map[string]interface{} `json:"ratios"`
|
||||||
|
}
|
||||||
76
server/admin/internal/modules/giftdiamond/handler.go
Normal file
76
server/admin/internal/modules/giftdiamond/handler.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package giftdiamond
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
|
"hyapp-admin-server/internal/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *Service
|
||||||
|
audit shared.OperationLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(walletDB *sql.DB, audit shared.OperationLogger) *Handler {
|
||||||
|
return &Handler{service: NewService(walletDB), audit: audit}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) List(c *gin.Context) {
|
||||||
|
regionID, ok := optionalInt64Query(c, "region_id", "regionId")
|
||||||
|
if !ok || regionID < 0 {
|
||||||
|
response.BadRequest(c, "区域不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := h.service.List(c.Request.Context(), appctx.FromContext(c.Request.Context()), regionID)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取礼物钻石比例失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalInt64Query(c *gin.Context, keys ...string) (int64, bool) {
|
||||||
|
for _, key := range keys {
|
||||||
|
value := strings.TrimSpace(c.Query(key))
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return parsed, true
|
||||||
|
}
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Update(c *gin.Context) {
|
||||||
|
var req updateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "礼物钻石比例参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := h.service.Update(c.Request.Context(), appctx.FromContext(c.Request.Context()), req, int64(shared.ActorFromContext(c).UserID))
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "gift-diamond-ratio-update", "gift_diamond_ratio_configs", fmt.Sprintf("region:%d", result.RegionID), "success",
|
||||||
|
fmt.Sprintf("region_id=%d ratios=%s", result.RegionID, strings.Join(ratioSummary(result.Items), ",")))
|
||||||
|
response.OK(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ratioSummary(items []ratioDTO) []string {
|
||||||
|
result := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
result = append(result, item.GiftTypeCode+"="+item.RatioPercent)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
15
server/admin/internal/modules/giftdiamond/routes.go
Normal file
15
server/admin/internal/modules/giftdiamond/routes.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package giftdiamond
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hyapp-admin-server/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
protected.GET("/admin/operations/gift-diamond-ratios", middleware.RequirePermission("gift-diamond:view"), h.List)
|
||||||
|
protected.PUT("/admin/operations/gift-diamond-ratios", middleware.RequirePermission("gift-diamond:update"), h.Update)
|
||||||
|
}
|
||||||
166
server/admin/internal/modules/giftdiamond/service.go
Normal file
166
server/admin/internal/modules/giftdiamond/service.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package giftdiamond
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const statusActive = "active"
|
||||||
|
|
||||||
|
var giftTypes = []string{"normal", "lucky", "super_lucky"}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(db *sql.DB) *Service {
|
||||||
|
return &Service{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) List(ctx context.Context, appCode string, regionID int64) (ratioListDTO, error) {
|
||||||
|
if err := s.ensureSchema(ctx); err != nil {
|
||||||
|
return ratioListDTO{}, err
|
||||||
|
}
|
||||||
|
items := make([]ratioDTO, 0, len(giftTypes))
|
||||||
|
for _, giftType := range giftTypes {
|
||||||
|
item, err := s.resolveRatio(ctx, appCode, regionID, giftType)
|
||||||
|
if err != nil {
|
||||||
|
return ratioListDTO{}, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return ratioListDTO{RegionID: regionID, Items: items}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, appCode string, req updateRequest, adminID int64) (ratioListDTO, error) {
|
||||||
|
if req.RegionID < 0 {
|
||||||
|
return ratioListDTO{}, errors.New("区域不正确")
|
||||||
|
}
|
||||||
|
if err := s.ensureSchema(ctx); err != nil {
|
||||||
|
return ratioListDTO{}, err
|
||||||
|
}
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return ratioListDTO{}, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
for _, giftType := range giftTypes {
|
||||||
|
raw, exists := req.Ratios[giftType]
|
||||||
|
if !exists {
|
||||||
|
return ratioListDTO{}, fmt.Errorf("%s 比例不能为空", giftType)
|
||||||
|
}
|
||||||
|
ratio, err := normalizePercent(raw)
|
||||||
|
if err != nil {
|
||||||
|
return ratioListDTO{}, fmt.Errorf("%s 比例不正确", giftType)
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO gift_diamond_ratio_configs (
|
||||||
|
app_code, region_id, gift_type_code, status, ratio_percent,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
status = VALUES(status),
|
||||||
|
ratio_percent = VALUES(ratio_percent),
|
||||||
|
updated_by_admin_id = VALUES(updated_by_admin_id),
|
||||||
|
updated_at_ms = VALUES(updated_at_ms)`,
|
||||||
|
appCode, req.RegionID, giftType, ratio, adminID, adminID, nowMs, nowMs,
|
||||||
|
); err != nil {
|
||||||
|
return ratioListDTO{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return ratioListDTO{}, err
|
||||||
|
}
|
||||||
|
return s.List(ctx, appCode, req.RegionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) resolveRatio(ctx context.Context, appCode string, regionID int64, giftType string) (ratioDTO, error) {
|
||||||
|
if item, exists, err := s.getRatio(ctx, appCode, regionID, giftType); err != nil || exists {
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
if regionID != 0 {
|
||||||
|
if item, exists, err := s.getRatio(ctx, appCode, 0, giftType); err != nil || exists {
|
||||||
|
item.RegionID = regionID
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ratioDTO{GiftTypeCode: giftType, RatioPercent: "100.00", RegionID: regionID, EffectiveRegionID: 0, Status: statusActive}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getRatio(ctx context.Context, appCode string, regionID int64, giftType string) (ratioDTO, bool, error) {
|
||||||
|
var item ratioDTO
|
||||||
|
var percent string
|
||||||
|
err := s.db.QueryRowContext(ctx, `
|
||||||
|
SELECT region_id, gift_type_code, CAST(ratio_percent AS CHAR), status, updated_at_ms
|
||||||
|
FROM gift_diamond_ratio_configs
|
||||||
|
WHERE app_code = ? AND region_id = ? AND gift_type_code = ?
|
||||||
|
LIMIT 1`, appCode, regionID, giftType).Scan(&item.EffectiveRegionID, &item.GiftTypeCode, &percent, &item.Status, &item.UpdatedAtMS)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return ratioDTO{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return ratioDTO{}, false, err
|
||||||
|
}
|
||||||
|
item.RegionID = regionID
|
||||||
|
item.RatioPercent = formatPercentString(percent)
|
||||||
|
return item, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePercent(raw interface{}) (string, error) {
|
||||||
|
text := strings.TrimSpace(fmt.Sprint(raw))
|
||||||
|
if text == "" {
|
||||||
|
return "", errors.New("empty")
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseFloat(text, 64)
|
||||||
|
if err != nil || math.IsNaN(value) || math.IsInf(value, 0) || value < 0 || value > 100 {
|
||||||
|
return "", errors.New("invalid")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f", value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPercentString(raw string) string {
|
||||||
|
value, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
|
||||||
|
if err != nil {
|
||||||
|
return "100.00"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureSchema(ctx context.Context) error {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return errors.New("wallet db is not configured")
|
||||||
|
}
|
||||||
|
if _, err := s.db.ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '送礼用户区域,0 表示全局默认',
|
||||||
|
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
|
||||||
|
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '主播周期钻石入账比例,百分比',
|
||||||
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
|
||||||
|
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新后台用户 ID',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, region_id, gift_type_code),
|
||||||
|
KEY idx_gift_diamond_ratio_region (app_code, region_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物入主播周期钻石比例配置表'`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := s.db.ExecContext(ctx, `
|
||||||
|
INSERT IGNORE INTO gift_diamond_ratio_configs (
|
||||||
|
app_code, region_id, gift_type_code, status, ratio_percent,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES
|
||||||
|
(?, 0, 'normal', 'active', 100.00, 0, 0, 0, 0),
|
||||||
|
(?, 0, 'lucky', 'active', 100.00, 0, 0, 0, 0),
|
||||||
|
(?, 0, 'super_lucky', 'active', 100.00, 0, 0, 0, 0)`,
|
||||||
|
"lalu", "lalu", "lalu")
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -88,6 +88,8 @@ var defaultPermissions = []model.Permission{
|
|||||||
{Name: "金币增减查看", Code: "coin-adjustment:view", Kind: "menu"},
|
{Name: "金币增减查看", Code: "coin-adjustment:view", Kind: "menu"},
|
||||||
{Name: "金币增减创建", Code: "coin-adjustment:create", Kind: "button"},
|
{Name: "金币增减创建", Code: "coin-adjustment:create", Kind: "button"},
|
||||||
{Name: "举报列表查看", Code: "report:view", Kind: "menu"},
|
{Name: "举报列表查看", Code: "report:view", Kind: "menu"},
|
||||||
|
{Name: "礼物钻石查看", Code: "gift-diamond:view", Kind: "menu"},
|
||||||
|
{Name: "礼物钻石更新", Code: "gift-diamond:update", Kind: "button"},
|
||||||
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
|
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
|
||||||
{Name: "内购配置查看", Code: "payment-product:view", Kind: "menu"},
|
{Name: "内购配置查看", Code: "payment-product:view", Kind: "menu"},
|
||||||
{Name: "内购配置创建", Code: "payment-product:create", Kind: "button"},
|
{Name: "内购配置创建", Code: "payment-product:create", Kind: "button"},
|
||||||
@ -251,6 +253,7 @@ func (s *Store) seedMenus() error {
|
|||||||
{ParentID: &operationsID, Title: "金币增减", Code: "operation-coin-adjustment", Path: "/operations/coin-adjustments", Icon: "wallet", PermissionCode: "coin-adjustment:view", Sort: 69, Visible: true},
|
{ParentID: &operationsID, Title: "金币增减", Code: "operation-coin-adjustment", Path: "/operations/coin-adjustments", Icon: "wallet", PermissionCode: "coin-adjustment:view", Sort: 69, Visible: true},
|
||||||
{ParentID: &operationsID, Title: "幸运礼物", Code: "lucky-gift", Path: "/operations/lucky-gift", Icon: "redeem", PermissionCode: "lucky-gift:view", Sort: 70, Visible: true},
|
{ParentID: &operationsID, Title: "幸运礼物", Code: "lucky-gift", Path: "/operations/lucky-gift", Icon: "redeem", PermissionCode: "lucky-gift:view", Sort: 70, Visible: true},
|
||||||
{ParentID: &operationsID, Title: "举报列表", Code: "operation-reports", Path: "/operations/reports", Icon: "flag", PermissionCode: "report:view", Sort: 71, Visible: true},
|
{ParentID: &operationsID, Title: "举报列表", Code: "operation-reports", Path: "/operations/reports", Icon: "flag", PermissionCode: "report:view", Sort: 71, Visible: true},
|
||||||
|
{ParentID: &operationsID, Title: "礼物钻石", Code: "operation-gift-diamond", Path: "/operations/gift-diamonds", Icon: "diamond", PermissionCode: "gift-diamond:view", Sort: 72, Visible: true},
|
||||||
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
|
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
|
||||||
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
|
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
|
||||||
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
|
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
|
||||||
@ -497,7 +500,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"agency:view", "agency:create", "agency:status",
|
"agency:view", "agency:create", "agency:status",
|
||||||
"bd:view", "bd:create", "bd:update",
|
"bd:view", "bd:create", "bd:update",
|
||||||
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
|
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
|
||||||
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||||
"lucky-gift:view", "lucky-gift:update",
|
"lucky-gift:view", "lucky-gift:update",
|
||||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||||
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
||||||
@ -511,7 +514,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"upload:create",
|
"upload:create",
|
||||||
}
|
}
|
||||||
case "auditor":
|
case "auditor":
|
||||||
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-adjustment:view", "report:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view", "role:view", "permission:view", "job:view"}
|
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view", "role:view", "permission:view", "job:view"}
|
||||||
case "readonly":
|
case "readonly":
|
||||||
return []string{
|
return []string{
|
||||||
"overview:view",
|
"overview:view",
|
||||||
@ -542,6 +545,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"coin-ledger:view",
|
"coin-ledger:view",
|
||||||
"coin-adjustment:view",
|
"coin-adjustment:view",
|
||||||
"report:view",
|
"report:view",
|
||||||
|
"gift-diamond:view",
|
||||||
"lucky-gift:view",
|
"lucky-gift:view",
|
||||||
"payment-bill:view",
|
"payment-bill:view",
|
||||||
"payment-product:view",
|
"payment-product:view",
|
||||||
@ -584,7 +588,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"region:view", "region:create", "region:update", "region:status",
|
"region:view", "region:create", "region:update", "region:status",
|
||||||
"host-agency-policy:view", "host-agency-policy:create", "host-agency-policy:update", "host-agency-policy:delete", "host-agency-policy:publish", "team-salary-policy:view", "team-salary-policy:create", "team-salary-policy:update", "team-salary-policy:delete", "host-salary-settlement:view", "host-salary-settlement:settle",
|
"host-agency-policy:view", "host-agency-policy:create", "host-agency-policy:update", "host-agency-policy:delete", "host-agency-policy:publish", "team-salary-policy:view", "team-salary-policy:create", "team-salary-policy:update", "team-salary-policy:delete", "host-salary-settlement:view", "host-salary-settlement:settle",
|
||||||
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
|
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
|
||||||
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||||
"lucky-gift:view", "lucky-gift:update",
|
"lucky-gift:view", "lucky-gift:update",
|
||||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||||
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
||||||
@ -595,7 +599,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"vip-config:view", "vip-config:update", "vip-config:grant",
|
"vip-config:view", "vip-config:update", "vip-config:grant",
|
||||||
}
|
}
|
||||||
case "auditor", "readonly":
|
case "auditor", "readonly":
|
||||||
return []string{"level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "report:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view"}
|
return []string{"level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view"}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import (
|
|||||||
"hyapp-admin-server/internal/modules/dashboard"
|
"hyapp-admin-server/internal/modules/dashboard"
|
||||||
"hyapp-admin-server/internal/modules/firstrechargereward"
|
"hyapp-admin-server/internal/modules/firstrechargereward"
|
||||||
gamemanagement "hyapp-admin-server/internal/modules/gamemanagement"
|
gamemanagement "hyapp-admin-server/internal/modules/gamemanagement"
|
||||||
|
"hyapp-admin-server/internal/modules/giftdiamond"
|
||||||
"hyapp-admin-server/internal/modules/health"
|
"hyapp-admin-server/internal/modules/health"
|
||||||
"hyapp-admin-server/internal/modules/hostagencypolicy"
|
"hyapp-admin-server/internal/modules/hostagencypolicy"
|
||||||
"hyapp-admin-server/internal/modules/hostorg"
|
"hyapp-admin-server/internal/modules/hostorg"
|
||||||
@ -60,6 +61,7 @@ type Handlers struct {
|
|||||||
Dashboard *dashboard.Handler
|
Dashboard *dashboard.Handler
|
||||||
FirstRechargeReward *firstrechargereward.Handler
|
FirstRechargeReward *firstrechargereward.Handler
|
||||||
Game *gamemanagement.Handler
|
Game *gamemanagement.Handler
|
||||||
|
GiftDiamond *giftdiamond.Handler
|
||||||
Health *health.Handler
|
Health *health.Handler
|
||||||
HostAgencyPolicy *hostagencypolicy.Handler
|
HostAgencyPolicy *hostagencypolicy.Handler
|
||||||
HostOrg *hostorg.Handler
|
HostOrg *hostorg.Handler
|
||||||
@ -115,6 +117,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
|||||||
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
|
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
|
||||||
regionblock.RegisterRoutes(protected, h.RegionBlock)
|
regionblock.RegisterRoutes(protected, h.RegionBlock)
|
||||||
gamemanagement.RegisterRoutes(protected, h.Game)
|
gamemanagement.RegisterRoutes(protected, h.Game)
|
||||||
|
giftdiamond.RegisterRoutes(protected, h.GiftDiamond)
|
||||||
roomadmin.RegisterRoutes(protected, h.RoomAdmin)
|
roomadmin.RegisterRoutes(protected, h.RoomAdmin)
|
||||||
roomtreasure.RegisterRoutes(protected, h.RoomTreasure)
|
roomtreasure.RegisterRoutes(protected, h.RoomTreasure)
|
||||||
dashboard.RegisterRoutes(protected, h.Dashboard)
|
dashboard.RegisterRoutes(protected, h.Dashboard)
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||||
|
|
||||||
|
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||||
|
('礼物钻石查看', 'gift-diamond:view', 'menu', '', @now_ms, @now_ms),
|
||||||
|
('礼物钻石更新', 'gift-diamond:update', 'button', '', @now_ms, @now_ms)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
kind = VALUES(kind),
|
||||||
|
description = VALUES(description),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms) VALUES
|
||||||
|
(NULL, '运营管理', 'operations', '', 'operations', '', 68, TRUE, @now_ms, @now_ms)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
title = VALUES(title),
|
||||||
|
path = VALUES(path),
|
||||||
|
icon = VALUES(icon),
|
||||||
|
permission_code = VALUES(permission_code),
|
||||||
|
sort = VALUES(sort),
|
||||||
|
visible = VALUES(visible),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
|
||||||
|
SELECT parent.id, '礼物钻石', 'operation-gift-diamond', '/operations/gift-diamonds', 'diamond', 'gift-diamond:view', 72, TRUE, @now_ms, @now_ms
|
||||||
|
FROM admin_menus parent
|
||||||
|
WHERE parent.code = 'operations'
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
parent_id = VALUES(parent_id),
|
||||||
|
title = VALUES(title),
|
||||||
|
path = VALUES(path),
|
||||||
|
icon = VALUES(icon),
|
||||||
|
permission_code = VALUES(permission_code),
|
||||||
|
sort = VALUES(sort),
|
||||||
|
visible = VALUES(visible),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||||
|
SELECT admin_role.id, admin_permission.id
|
||||||
|
FROM admin_roles admin_role
|
||||||
|
JOIN admin_permissions admin_permission
|
||||||
|
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
|
||||||
|
AND admin_permission.code IN ('gift-diamond:view', 'gift-diamond:update');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||||
|
SELECT admin_role.id, admin_permission.id
|
||||||
|
FROM admin_roles admin_role
|
||||||
|
JOIN admin_permissions admin_permission
|
||||||
|
WHERE admin_role.code IN ('auditor', 'readonly')
|
||||||
|
AND admin_permission.code = 'gift-diamond:view';
|
||||||
@ -56,7 +56,7 @@ func (h *Handler) getAppBootstrap(writer http.ResponseWriter, request *http.Requ
|
|||||||
FeatureFlags: map[string]bool{
|
FeatureFlags: map[string]bool{
|
||||||
"wallet_recharge": true,
|
"wallet_recharge": true,
|
||||||
"wallet_withdraw": true,
|
"wallet_withdraw": true,
|
||||||
"diamond_exchange": true,
|
"diamond_exchange": false,
|
||||||
"message_tab": true,
|
"message_tab": true,
|
||||||
"host_center": true,
|
"host_center": true,
|
||||||
"room_create": true,
|
"room_create": true,
|
||||||
|
|||||||
@ -1259,6 +1259,11 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
senderRegionID, err := h.resolveGiftSenderRegionID(request)
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
targetIsHost, targetHostRegionID, targetAgencyOwnerUserID, err := h.resolveGiftTargetHostScope(request, firstUserID(targetUserIDs))
|
targetIsHost, targetHostRegionID, targetAgencyOwnerUserID, err := h.resolveGiftTargetHostScope(request, firstUserID(targetUserIDs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
@ -1273,6 +1278,7 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
|||||||
GiftId: body.GiftID,
|
GiftId: body.GiftID,
|
||||||
GiftCount: body.GiftCount,
|
GiftCount: body.GiftCount,
|
||||||
PoolId: body.PoolID,
|
PoolId: body.PoolID,
|
||||||
|
SenderRegionId: senderRegionID,
|
||||||
TargetIsHost: targetIsHost,
|
TargetIsHost: targetIsHost,
|
||||||
TargetHostRegionId: targetHostRegionID,
|
TargetHostRegionId: targetHostRegionID,
|
||||||
TargetAgencyOwnerUserId: targetAgencyOwnerUserID,
|
TargetAgencyOwnerUserId: targetAgencyOwnerUserID,
|
||||||
@ -1280,6 +1286,23 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
|||||||
httpkit.Write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) resolveGiftSenderRegionID(request *http.Request) (int64, error) {
|
||||||
|
if h.userProfileClient == nil {
|
||||||
|
return 0, xerr.New(xerr.Unavailable, "user service is not configured")
|
||||||
|
}
|
||||||
|
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if resp.GetUser() == nil {
|
||||||
|
return 0, xerr.New(xerr.NotFound, "sender not found")
|
||||||
|
}
|
||||||
|
return resp.GetUser().GetRegionId(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) resolveGiftTargetHostScope(request *http.Request, targetUserID int64) (bool, int64, int64, error) {
|
func (h *Handler) resolveGiftTargetHostScope(request *http.Request, targetUserID int64) (bool, int64, int64, error) {
|
||||||
if targetUserID <= 0 || h.userHostClient == nil {
|
if targetUserID <= 0 || h.userHostClient == nil {
|
||||||
// 未装配 host client 时送礼仍可继续,但工资周期钻石 fail-closed,避免客户端伪造主播入账。
|
// 未装配 host client 时送礼仍可继续,但工资周期钻石 fail-closed,避免客户端伪造主播入账。
|
||||||
|
|||||||
@ -368,6 +368,8 @@ type SendGift struct {
|
|||||||
PoolID string `json:"pool_id,omitempty"`
|
PoolID string `json:"pool_id,omitempty"`
|
||||||
// GiftCount 是礼物数量,必须为正数。
|
// GiftCount 是礼物数量,必须为正数。
|
||||||
GiftCount int32 `json:"gift_count"`
|
GiftCount int32 `json:"gift_count"`
|
||||||
|
// SenderRegionID 是送礼用户所属区域,由 gateway 从 user-service 注入。
|
||||||
|
SenderRegionID int64 `json:"sender_region_id,omitempty"`
|
||||||
// TargetIsHost 是 gateway 从 user-service active host profile 注入的工资入账开关。
|
// TargetIsHost 是 gateway 从 user-service active host profile 注入的工资入账开关。
|
||||||
TargetIsHost bool `json:"target_is_host,omitempty"`
|
TargetIsHost bool `json:"target_is_host,omitempty"`
|
||||||
// TargetHostRegionID 是工资政策匹配区域;room-service 不自行推导主播身份或区域。
|
// TargetHostRegionID 是工资政策匹配区域;room-service 不自行推导主播身份或区域。
|
||||||
|
|||||||
@ -24,13 +24,14 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
|||||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||||
// SendGift 的账务不在 room-service 内结算,但房间表现、热度和榜单由 Room Cell 同步更新。
|
// SendGift 的账务不在 room-service 内结算,但房间表现、热度和榜单由 Room Cell 同步更新。
|
||||||
cmd := command.SendGift{
|
cmd := command.SendGift{
|
||||||
Base: baseFromMeta(req.GetMeta()),
|
Base: baseFromMeta(req.GetMeta()),
|
||||||
TargetType: normalizeGiftTargetType(req.GetTargetType()),
|
TargetType: normalizeGiftTargetType(req.GetTargetType()),
|
||||||
TargetUserIDs: normalizeGiftTargetUserIDs(req.GetTargetUserId(), req.GetTargetUserIds()),
|
TargetUserIDs: normalizeGiftTargetUserIDs(req.GetTargetUserId(), req.GetTargetUserIds()),
|
||||||
GiftID: req.GetGiftId(),
|
GiftID: req.GetGiftId(),
|
||||||
PoolID: strings.TrimSpace(req.GetPoolId()),
|
PoolID: strings.TrimSpace(req.GetPoolId()),
|
||||||
GiftCount: req.GetGiftCount(),
|
GiftCount: req.GetGiftCount(),
|
||||||
TargetIsHost: req.GetTargetIsHost(),
|
SenderRegionID: req.GetSenderRegionId(),
|
||||||
|
TargetIsHost: req.GetTargetIsHost(),
|
||||||
// 工资区域由 gateway 从 user-service host profile 注入;房间 visible_region_id 只用于房间/礼物可见性。
|
// 工资区域由 gateway 从 user-service host profile 注入;房间 visible_region_id 只用于房间/礼物可见性。
|
||||||
TargetHostRegionID: req.GetTargetHostRegionId(),
|
TargetHostRegionID: req.GetTargetHostRegionId(),
|
||||||
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
|
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
|
||||||
@ -115,6 +116,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
|||||||
GiftCount: cmd.GiftCount,
|
GiftCount: cmd.GiftCount,
|
||||||
AppCode: appcode.FromContext(ctx),
|
AppCode: appcode.FromContext(ctx),
|
||||||
RegionId: roomMeta.VisibleRegionID,
|
RegionId: roomMeta.VisibleRegionID,
|
||||||
|
SenderRegionId: cmd.SenderRegionID,
|
||||||
TargetIsHost: cmd.TargetIsHost,
|
TargetIsHost: cmd.TargetIsHost,
|
||||||
TargetHostRegionId: cmd.TargetHostRegionID,
|
TargetHostRegionId: cmd.TargetHostRegionID,
|
||||||
TargetAgencyOwnerUserId: cmd.TargetAgencyOwnerUserID,
|
TargetAgencyOwnerUserId: cmd.TargetAgencyOwnerUserID,
|
||||||
|
|||||||
@ -902,6 +902,20 @@ CREATE TABLE IF NOT EXISTS wallet_diamond_exchange_rules (
|
|||||||
KEY idx_diamond_exchange_active (app_code, status, sort_order)
|
KEY idx_diamond_exchange_active (app_code, status, sort_order)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包钻石兑换规则表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包钻石兑换规则表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '送礼用户区域,0 表示全局默认',
|
||||||
|
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
|
||||||
|
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '主播周期钻石入账比例,百分比',
|
||||||
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
|
||||||
|
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新后台用户 ID',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, region_id, gift_type_code),
|
||||||
|
KEY idx_gift_diamond_ratio_region (app_code, region_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物入主播周期钻石比例配置表';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS red_packet_configs (
|
CREATE TABLE IF NOT EXISTS red_packet_configs (
|
||||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否启用红包',
|
enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否启用红包',
|
||||||
@ -1139,13 +1153,6 @@ INSERT IGNORE INTO vip_levels (
|
|||||||
('lalu', 9, 'VIP9', 'disabled', 0, 604800000, 0, 0, 90, 0, 0),
|
('lalu', 9, 'VIP9', 'disabled', 0, 604800000, 0, 0, 90, 0, 0),
|
||||||
('lalu', 10, 'VIP10', 'disabled', 0, 604800000, 0, 0, 100, 0, 0);
|
('lalu', 10, 'VIP10', 'disabled', 0, 604800000, 0, 0, 100, 0, 0);
|
||||||
|
|
||||||
INSERT IGNORE INTO wallet_diamond_exchange_rules (
|
|
||||||
app_code, exchange_type, status, from_asset_type, to_asset_type,
|
|
||||||
from_amount, to_amount, sort_order, created_at_ms, updated_at_ms
|
|
||||||
) VALUES
|
|
||||||
('lalu', 'diamond_to_coin', 'active', 'DIAMOND', 'COIN', 100, 100, 10, 0, 0),
|
|
||||||
('lalu', 'diamond_to_usd_balance', 'active', 'DIAMOND', 'USD_BALANCE', 1000, 100, 20, 0, 0);
|
|
||||||
|
|
||||||
INSERT IGNORE INTO gift_type_configs (
|
INSERT IGNORE INTO gift_type_configs (
|
||||||
app_code, type_code, name, tab_key, status, sort_order,
|
app_code, type_code, name, tab_key, status, sort_order,
|
||||||
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||||
@ -1160,3 +1167,11 @@ INSERT IGNORE INTO gift_type_configs (
|
|||||||
('lalu', 'activity', '活动礼物', 'Activity', 'active', 80, 0, 0, 0, 0),
|
('lalu', 'activity', '活动礼物', 'Activity', 'active', 80, 0, 0, 0, 0),
|
||||||
('lalu', 'magic', '魔法礼物', 'Magic', 'active', 90, 0, 0, 0, 0),
|
('lalu', 'magic', '魔法礼物', 'Magic', 'active', 90, 0, 0, 0, 0),
|
||||||
('lalu', 'custom', '定制礼物', 'Custom', 'active', 100, 0, 0, 0, 0);
|
('lalu', 'custom', '定制礼物', 'Custom', 'active', 100, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO gift_diamond_ratio_configs (
|
||||||
|
app_code, region_id, gift_type_code, status, ratio_percent,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES
|
||||||
|
('lalu', 0, 'normal', 'active', 100.00, 0, 0, 0, 0),
|
||||||
|
('lalu', 0, 'lucky', 'active', 100.00, 0, 0, 0, 0),
|
||||||
|
('lalu', 0, 'super_lucky', 'active', 100.00, 0, 0, 0, 0);
|
||||||
|
|||||||
@ -9,8 +9,6 @@ const (
|
|||||||
AssetCoinSellerCoin = "COIN_SELLER_COIN"
|
AssetCoinSellerCoin = "COIN_SELLER_COIN"
|
||||||
// AssetGiftPoint 是主播礼物积分,不能直接消费或提现。
|
// AssetGiftPoint 是主播礼物积分,不能直接消费或提现。
|
||||||
AssetGiftPoint = "GIFT_POINT"
|
AssetGiftPoint = "GIFT_POINT"
|
||||||
// AssetDiamond 是后续兑换用资产,当前只在余额模型中保留。
|
|
||||||
AssetDiamond = "DIAMOND"
|
|
||||||
// AssetHostPeriodDiamond 是主播工资周期钻石投影,只参与工资等级结算,不进入通用钱包余额。
|
// AssetHostPeriodDiamond 是主播工资周期钻石投影,只参与工资等级结算,不进入通用钱包余额。
|
||||||
AssetHostPeriodDiamond = "HOST_PERIOD_DIAMOND"
|
AssetHostPeriodDiamond = "HOST_PERIOD_DIAMOND"
|
||||||
// AssetUSDBalance 是主播可提现美元余额,单位由后续提现策略固定。
|
// AssetUSDBalance 是主播可提现美元余额,单位由后续提现策略固定。
|
||||||
@ -109,6 +107,8 @@ type DebitGiftCommand struct {
|
|||||||
GiftCount int32
|
GiftCount int32
|
||||||
PriceVersion string
|
PriceVersion string
|
||||||
RegionID int64
|
RegionID int64
|
||||||
|
// SenderRegionID 是送礼用户所属区域;用于按区域匹配礼物入主播周期钻石比例。
|
||||||
|
SenderRegionID int64
|
||||||
// TargetIsHost 只能由 gateway 根据 user-service active host profile 注入,客户端输入不可信。
|
// TargetIsHost 只能由 gateway 根据 user-service active host profile 注入,客户端输入不可信。
|
||||||
TargetIsHost bool
|
TargetIsHost bool
|
||||||
// TargetHostRegionID 是主播身份所属区域;工资政策按该区域匹配,不能用房间可见区域替代。
|
// TargetHostRegionID 是主播身份所属区域;工资政策按该区域匹配,不能用房间可见区域替代。
|
||||||
@ -786,7 +786,7 @@ type RedPacketExpireResult struct {
|
|||||||
// ValidAssetType 限定账本允许落账的资产类型,避免写入任意字符串资产。
|
// ValidAssetType 限定账本允许落账的资产类型,避免写入任意字符串资产。
|
||||||
func ValidAssetType(assetType string) bool {
|
func ValidAssetType(assetType string) bool {
|
||||||
switch assetType {
|
switch assetType {
|
||||||
case AssetCoin, AssetCoinSellerCoin, AssetGiftPoint, AssetDiamond, AssetUSDBalance:
|
case AssetCoin, AssetCoinSellerCoin, AssetGiftPoint, AssetUSDBalance:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@ -794,12 +794,7 @@ func ValidAssetType(assetType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NormalizeGiftChargeAssetType(assetType string) string {
|
func NormalizeGiftChargeAssetType(assetType string) string {
|
||||||
switch strings.ToUpper(strings.TrimSpace(assetType)) {
|
return AssetCoin
|
||||||
case AssetDiamond:
|
|
||||||
return AssetDiamond
|
|
||||||
default:
|
|
||||||
return AssetCoin
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NormalizeVipGrantSource(source string) string {
|
func NormalizeVipGrantSource(source string) string {
|
||||||
@ -817,7 +812,7 @@ func NormalizeVipGrantSource(source string) string {
|
|||||||
|
|
||||||
func ValidGiftChargeAssetType(assetType string) bool {
|
func ValidGiftChargeAssetType(assetType string) bool {
|
||||||
switch strings.ToUpper(strings.TrimSpace(assetType)) {
|
switch strings.ToUpper(strings.TrimSpace(assetType)) {
|
||||||
case AssetCoin, AssetDiamond:
|
case AssetCoin:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -145,6 +145,53 @@ func TestDebitGiftCreditsHostPeriodDiamondsOnlyForHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDebitGiftCreditsHostPeriodDiamondsBySenderRegionAndGiftType(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
repository.SetGiftDiamondRatio(1001, resourcedomain.GiftTypeNormal, "30.00")
|
||||||
|
repository.SetGiftDiamondRatio(1001, resourcedomain.GiftTypeLucky, "40.00")
|
||||||
|
repository.SetGiftDiamondRatio(1001, resourcedomain.GiftTypeSuperLucky, "50.00")
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
giftID string
|
||||||
|
giftType string
|
||||||
|
commandID string
|
||||||
|
senderID int64
|
||||||
|
wantDiamond int64
|
||||||
|
}{
|
||||||
|
{giftID: "normal-ratio-gift", giftType: resourcedomain.GiftTypeNormal, commandID: "cmd-normal-ratio", senderID: 11001, wantDiamond: 30},
|
||||||
|
{giftID: "lucky-ratio-gift", giftType: resourcedomain.GiftTypeLucky, commandID: "cmd-lucky-ratio", senderID: 11002, wantDiamond: 40},
|
||||||
|
{giftID: "super-lucky-ratio-gift", giftType: resourcedomain.GiftTypeSuperLucky, commandID: "cmd-super-lucky-ratio", senderID: 11003, wantDiamond: 50},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
repository.SetBalance(tc.senderID, 100)
|
||||||
|
repository.SetGiftPrice(tc.giftID, "v1", 100, 100, 100)
|
||||||
|
repository.SetGiftType(tc.giftID, tc.giftType)
|
||||||
|
receipt, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
||||||
|
CommandID: tc.commandID,
|
||||||
|
RoomID: "room-ratio",
|
||||||
|
SenderUserID: tc.senderID,
|
||||||
|
SenderRegionID: 1001,
|
||||||
|
TargetUserID: 12001,
|
||||||
|
GiftID: tc.giftID,
|
||||||
|
GiftCount: 1,
|
||||||
|
PriceVersion: "v1",
|
||||||
|
TargetIsHost: true,
|
||||||
|
TargetHostRegionID: 8801,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DebitGift %s failed: %v", tc.giftType, err)
|
||||||
|
}
|
||||||
|
if receipt.HostPeriodDiamondAdded != tc.wantDiamond {
|
||||||
|
t.Fatalf("%s ratio diamond mismatch: got %+v want %d", tc.giftType, receipt, tc.wantDiamond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := repository.HostPeriodGiftDiamondTotal(12001); got != 120 {
|
||||||
|
t.Fatalf("host period account total mismatch, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestDebitGiftRejectsHostPeriodWithoutRegion 锁定工资政策必须有主播区域才能入账。
|
// TestDebitGiftRejectsHostPeriodWithoutRegion 锁定工资政策必须有主播区域才能入账。
|
||||||
func TestDebitGiftRejectsHostPeriodWithoutRegion(t *testing.T) {
|
func TestDebitGiftRejectsHostPeriodWithoutRegion(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
@ -1581,7 +1628,7 @@ func TestGiftConfigMustSelectGiftResource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGiftConfigSupportsDiamondChargeAndEffectiveRange(t *testing.T) {
|
func TestGiftConfigCoercesDiamondChargeToCoinAndEffectiveRange(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
svc := walletservice.New(repository)
|
svc := walletservice.New(repository)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@ -1606,7 +1653,7 @@ func TestGiftConfigSupportsDiamondChargeAndEffectiveRange(t *testing.T) {
|
|||||||
Status: resourcedomain.StatusActive,
|
Status: resourcedomain.StatusActive,
|
||||||
Name: "Diamond Magic",
|
Name: "Diamond Magic",
|
||||||
PriceVersion: "v1",
|
PriceVersion: "v1",
|
||||||
ChargeAssetType: ledger.AssetDiamond,
|
ChargeAssetType: "DIAMOND",
|
||||||
CoinPrice: 7,
|
CoinPrice: 7,
|
||||||
GiftPointAmount: 3,
|
GiftPointAmount: 3,
|
||||||
HeatValue: 9,
|
HeatValue: 9,
|
||||||
@ -1620,11 +1667,11 @@ func TestGiftConfigSupportsDiamondChargeAndEffectiveRange(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create diamond gift config failed: %v", err)
|
t.Fatalf("create diamond gift config failed: %v", err)
|
||||||
}
|
}
|
||||||
if gift.ChargeAssetType != ledger.AssetDiamond || gift.GiftTypeCode != resourcedomain.GiftTypeMagic || len(gift.EffectTypes) != 2 {
|
if gift.ChargeAssetType != ledger.AssetCoin || gift.GiftTypeCode != resourcedomain.GiftTypeMagic || len(gift.EffectTypes) != 2 {
|
||||||
t.Fatalf("diamond gift config mismatch: %+v", gift)
|
t.Fatalf("gift config must not keep DIAMOND charge: %+v", gift)
|
||||||
}
|
}
|
||||||
|
|
||||||
repository.SetAssetBalance(52001, ledger.AssetDiamond, 100)
|
repository.SetBalance(52001, 100)
|
||||||
receipt, err := svc.DebitGift(ctx, ledger.DebitGiftCommand{
|
receipt, err := svc.DebitGift(ctx, ledger.DebitGiftCommand{
|
||||||
CommandID: "cmd-diamond-gift",
|
CommandID: "cmd-diamond-gift",
|
||||||
RoomID: "room-diamond",
|
RoomID: "room-diamond",
|
||||||
@ -1636,8 +1683,8 @@ func TestGiftConfigSupportsDiamondChargeAndEffectiveRange(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DebitGift with diamond-backed gift failed: %v", err)
|
t.Fatalf("DebitGift with diamond-backed gift failed: %v", err)
|
||||||
}
|
}
|
||||||
if receipt.ChargeAssetType != ledger.AssetDiamond || receipt.ChargeAmount != 14 || receipt.BalanceAfter != 86 {
|
if receipt.ChargeAssetType != ledger.AssetCoin || receipt.ChargeAmount != 14 || receipt.BalanceAfter != 86 {
|
||||||
t.Fatalf("diamond gift settlement mismatch: %+v", receipt)
|
t.Fatalf("gift settlement must use COIN after DIAMOND removal: %+v", receipt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1670,7 +1717,7 @@ func TestGiftConfigSyncsConfiguredResourcePrice(t *testing.T) {
|
|||||||
Status: resourcedomain.StatusActive,
|
Status: resourcedomain.StatusActive,
|
||||||
Name: "Price Sync Gift",
|
Name: "Price Sync Gift",
|
||||||
PriceVersion: "v1",
|
PriceVersion: "v1",
|
||||||
ChargeAssetType: ledger.AssetDiamond,
|
ChargeAssetType: "DIAMOND",
|
||||||
CoinPrice: 1,
|
CoinPrice: 1,
|
||||||
GiftPointAmount: 1,
|
GiftPointAmount: 1,
|
||||||
HeatValue: 3,
|
HeatValue: 3,
|
||||||
@ -2788,29 +2835,28 @@ func vipLevelByNumber(levels []ledger.VipLevel, target int32) *ledger.VipLevel {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWalletOverviewAndDiamondExchangeConfigComeFromWalletService 验证我的页开关和钻石兑换配置不再由 gateway 硬编码。
|
// TestWalletOverviewDisablesDiamondExchange 验证普通 DIAMOND 钱包停用后钱包概览不再返回钻石余额或兑换入口。
|
||||||
func TestWalletOverviewAndDiamondExchangeConfigComeFromWalletService(t *testing.T) {
|
func TestWalletOverviewDisablesDiamondExchange(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
repository.SetBalance(52001, 700)
|
repository.SetBalance(52001, 700)
|
||||||
repository.SetAssetBalance(52001, ledger.AssetDiamond, 300)
|
|
||||||
svc := walletservice.New(repository)
|
svc := walletservice.New(repository)
|
||||||
|
|
||||||
overview, err := svc.GetWalletOverview(context.Background(), 52001)
|
overview, err := svc.GetWalletOverview(context.Background(), 52001)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetWalletOverview failed: %v", err)
|
t.Fatalf("GetWalletOverview failed: %v", err)
|
||||||
}
|
}
|
||||||
if !overview.FeatureFlags.RechargeEnabled || !overview.FeatureFlags.DiamondExchangeEnabled || !overview.FeatureFlags.WithdrawEnabled {
|
if !overview.FeatureFlags.RechargeEnabled || overview.FeatureFlags.DiamondExchangeEnabled || !overview.FeatureFlags.WithdrawEnabled {
|
||||||
t.Fatalf("wallet feature flags mismatch: %+v", overview.FeatureFlags)
|
t.Fatalf("wallet feature flags mismatch: %+v", overview.FeatureFlags)
|
||||||
}
|
}
|
||||||
if balanceAmount(overview.Balances, ledger.AssetCoin) != 700 || balanceAmount(overview.Balances, ledger.AssetDiamond) != 300 {
|
if balanceAmount(overview.Balances, ledger.AssetCoin) != 700 || balanceAmount(overview.Balances, "DIAMOND") != 0 {
|
||||||
t.Fatalf("overview balances mismatch: %+v", overview.Balances)
|
t.Fatalf("overview balances mismatch: %+v", overview.Balances)
|
||||||
}
|
}
|
||||||
rules, err := svc.GetDiamondExchangeConfig(context.Background(), 52001)
|
rules, err := svc.GetDiamondExchangeConfig(context.Background(), 52001)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetDiamondExchangeConfig failed: %v", err)
|
t.Fatalf("GetDiamondExchangeConfig failed: %v", err)
|
||||||
}
|
}
|
||||||
if len(rules) != 2 || rules[0].FromAssetType != ledger.AssetDiamond || !rules[0].Enabled {
|
if len(rules) != 0 {
|
||||||
t.Fatalf("diamond exchange rules mismatch: %+v", rules)
|
t.Fatalf("diamond exchange rules must be empty after DIAMOND removal: %+v", rules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const (
|
|||||||
|
|
||||||
// GetWalletOverview 返回钱包首页摘要;开关放在 wallet-service 内,避免 gateway 硬编码账务能力。
|
// GetWalletOverview 返回钱包首页摘要;开关放在 wallet-service 内,避免 gateway 硬编码账务能力。
|
||||||
func (r *Repository) GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error) {
|
func (r *Repository) GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error) {
|
||||||
balances, err := r.GetBalances(ctx, userID, []string{ledger.AssetCoin, ledger.AssetDiamond, ledger.AssetUSDBalance})
|
balances, err := r.GetBalances(ctx, userID, []string{ledger.AssetCoin, ledger.AssetUSDBalance})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.WalletOverview{}, err
|
return ledger.WalletOverview{}, err
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ func (r *Repository) GetWalletOverview(ctx context.Context, userID int64) (ledge
|
|||||||
Balances: balances,
|
Balances: balances,
|
||||||
FeatureFlags: ledger.WalletFeatureFlags{
|
FeatureFlags: ledger.WalletFeatureFlags{
|
||||||
RechargeEnabled: true,
|
RechargeEnabled: true,
|
||||||
DiamondExchangeEnabled: true,
|
DiamondExchangeEnabled: false,
|
||||||
WithdrawEnabled: true,
|
WithdrawEnabled: true,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@ -129,29 +129,7 @@ func (r *Repository) GetDiamondExchangeConfig(ctx context.Context, _ int64) ([]l
|
|||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
}
|
}
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
return []ledger.DiamondExchangeRule{}, nil
|
||||||
SELECT exchange_type, from_asset_type, to_asset_type, from_amount, to_amount, status
|
|
||||||
FROM wallet_diamond_exchange_rules
|
|
||||||
WHERE app_code = ? AND status = 'active'
|
|
||||||
ORDER BY sort_order ASC, rule_id ASC`,
|
|
||||||
appcode.FromContext(ctx),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
rules := make([]ledger.DiamondExchangeRule, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var rule ledger.DiamondExchangeRule
|
|
||||||
var status string
|
|
||||||
if err := rows.Scan(&rule.ExchangeType, &rule.FromAssetType, &rule.ToAssetType, &rule.FromAmount, &rule.ToAmount, &status); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rule.Enabled = status == "active"
|
|
||||||
rules = append(rules, rule)
|
|
||||||
}
|
|
||||||
return rules, rows.Err()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListWalletTransactions 分页读取当前用户钱包分录,并带出交易业务类型。
|
// ListWalletTransactions 分页读取当前用户钱包分录,并带出交易业务类型。
|
||||||
|
|||||||
@ -83,10 +83,42 @@ func Open(ctx context.Context, dsn string) (*Repository, error) {
|
|||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ensureGiftDiamondRatioSchema(ctx, db); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Repository{db: db}, nil
|
return &Repository{db: db}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureGiftDiamondRatioSchema(ctx context.Context, db *sql.DB) error {
|
||||||
|
if _, err := db.ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '送礼用户区域,0 表示全局默认',
|
||||||
|
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
|
||||||
|
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '主播周期钻石入账比例,百分比',
|
||||||
|
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
|
||||||
|
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新后台用户 ID',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, region_id, gift_type_code),
|
||||||
|
KEY idx_gift_diamond_ratio_region (app_code, region_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物入主播周期钻石比例配置表'`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := db.ExecContext(ctx, `
|
||||||
|
INSERT IGNORE INTO gift_diamond_ratio_configs (
|
||||||
|
app_code, region_id, gift_type_code, status, ratio_percent,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES
|
||||||
|
('lalu', 0, 'normal', 'active', 100.00, 0, 0, 0, 0),
|
||||||
|
('lalu', 0, 'lucky', 'active', 100.00, 0, 0, 0, 0),
|
||||||
|
('lalu', 0, 'super_lucky', 'active', 100.00, 0, 0, 0, 0)`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Close 释放 MySQL 连接池。
|
// Close 释放 MySQL 连接池。
|
||||||
func (r *Repository) Close() error {
|
func (r *Repository) Close() error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
@ -135,6 +167,9 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
if command.RegionID < 0 {
|
if command.RegionID < 0 {
|
||||||
return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "region_id is invalid")
|
return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "region_id is invalid")
|
||||||
}
|
}
|
||||||
|
if command.SenderRegionID < 0 {
|
||||||
|
return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "sender_region_id is invalid")
|
||||||
|
}
|
||||||
giftConfig, err := r.resolveActiveGiftConfig(ctx, tx, command.GiftID, command.RegionID)
|
giftConfig, err := r.resolveActiveGiftConfig(ctx, tx, command.GiftID, command.RegionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.Receipt{}, err
|
return ledger.Receipt{}, err
|
||||||
@ -157,10 +192,20 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
}
|
}
|
||||||
hostPeriodDiamondAdded := int64(0)
|
hostPeriodDiamondAdded := int64(0)
|
||||||
hostPeriodCycleKey := ""
|
hostPeriodCycleKey := ""
|
||||||
|
giftDiamondRatioPercent := "0.00"
|
||||||
|
giftDiamondRatioRegionID := int64(0)
|
||||||
if command.TargetIsHost {
|
if command.TargetIsHost {
|
||||||
// Phase 2 固化“用户礼物金币价值 -> 主播周期钻石”的默认 1:1 口径;后续政策配置只替换这里的换算快照。
|
ratio, ratioRegionID, err := r.resolveGiftDiamondRatio(ctx, tx, command.AppCode, command.SenderRegionID, giftConfig.GiftTypeCode)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.Receipt{}, err
|
||||||
|
}
|
||||||
// 非主播不会进入该分支,因此普通用户收礼只增加 GIFT_POINT,不产生工资周期积分。
|
// 非主播不会进入该分支,因此普通用户收礼只增加 GIFT_POINT,不产生工资周期积分。
|
||||||
hostPeriodDiamondAdded = chargeAmount
|
hostPeriodDiamondAdded, err = giftDiamondAmount(chargeAmount, ratio.PPM)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.Receipt{}, err
|
||||||
|
}
|
||||||
|
giftDiamondRatioPercent = ratio.Percent
|
||||||
|
giftDiamondRatioRegionID = ratioRegionID
|
||||||
hostPeriodCycleKey = hostPeriodCycleKeyFromMS(nowMs)
|
hostPeriodCycleKey = hostPeriodCycleKeyFromMS(nowMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,34 +223,37 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
|
|
||||||
transactionID := transactionID(command.AppCode, command.CommandID)
|
transactionID := transactionID(command.AppCode, command.CommandID)
|
||||||
metadata := giftMetadata{
|
metadata := giftMetadata{
|
||||||
AppCode: command.AppCode,
|
AppCode: command.AppCode,
|
||||||
GiftID: command.GiftID,
|
GiftID: command.GiftID,
|
||||||
GiftName: giftConfig.Name,
|
GiftName: giftConfig.Name,
|
||||||
ResourceID: giftConfig.ResourceID,
|
ResourceID: giftConfig.ResourceID,
|
||||||
ResourceSnapshot: mustJSON(giftConfig.Resource),
|
ResourceSnapshot: mustJSON(giftConfig.Resource),
|
||||||
GiftTypeCode: giftConfig.GiftTypeCode,
|
GiftTypeCode: giftConfig.GiftTypeCode,
|
||||||
PresentationJSON: giftConfig.PresentationJSON,
|
PresentationJSON: giftConfig.PresentationJSON,
|
||||||
SortOrder: giftConfig.SortOrder,
|
SortOrder: giftConfig.SortOrder,
|
||||||
GiftCount: command.GiftCount,
|
GiftCount: command.GiftCount,
|
||||||
PriceVersion: price.PriceVersion,
|
PriceVersion: price.PriceVersion,
|
||||||
ChargeAssetType: price.ChargeAssetType,
|
ChargeAssetType: price.ChargeAssetType,
|
||||||
ChargeAmount: chargeAmount,
|
ChargeAmount: chargeAmount,
|
||||||
CoinSpent: chargeAmount,
|
CoinSpent: chargeAmount,
|
||||||
GiftPointAdded: giftPointAdded,
|
GiftPointAdded: giftPointAdded,
|
||||||
HeatValue: heatValue,
|
HeatValue: heatValue,
|
||||||
BalanceAfter: sender.AvailableAmount - chargeAmount,
|
BalanceAfter: sender.AvailableAmount - chargeAmount,
|
||||||
BillingReceipt: billingReceiptID(command.AppCode, command.CommandID),
|
BillingReceipt: billingReceiptID(command.AppCode, command.CommandID),
|
||||||
SenderUserID: command.SenderUserID,
|
SenderUserID: command.SenderUserID,
|
||||||
TargetUserID: command.TargetUserID,
|
SenderRegionID: command.SenderRegionID,
|
||||||
TargetIsHost: command.TargetIsHost,
|
TargetUserID: command.TargetUserID,
|
||||||
TargetHostRegionID: command.TargetHostRegionID,
|
TargetIsHost: command.TargetIsHost,
|
||||||
TargetAgencyOwnerUserID: command.TargetAgencyOwnerUserID,
|
TargetHostRegionID: command.TargetHostRegionID,
|
||||||
HostPeriodDiamondAdded: hostPeriodDiamondAdded,
|
TargetAgencyOwnerUserID: command.TargetAgencyOwnerUserID,
|
||||||
HostPeriodCycleKey: hostPeriodCycleKey,
|
HostPeriodDiamondAdded: hostPeriodDiamondAdded,
|
||||||
RoomID: command.RoomID,
|
GiftDiamondRatioPercent: giftDiamondRatioPercent,
|
||||||
CoinPrice: price.CoinPrice,
|
GiftDiamondRatioRegionID: giftDiamondRatioRegionID,
|
||||||
GiftPointAmount: price.GiftPointAmount,
|
HostPeriodCycleKey: hostPeriodCycleKey,
|
||||||
HeatUnitValue: price.HeatValue,
|
RoomID: command.RoomID,
|
||||||
|
CoinPrice: price.CoinPrice,
|
||||||
|
GiftPointAmount: price.GiftPointAmount,
|
||||||
|
HeatUnitValue: price.HeatValue,
|
||||||
}
|
}
|
||||||
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeGiftDebit, requestHash, fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
|
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeGiftDebit, requestHash, fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
|
||||||
return ledger.Receipt{}, err
|
return ledger.Receipt{}, err
|
||||||
@ -339,12 +387,7 @@ func (r *Repository) upsertUserGiftWall(ctx context.Context, tx *sql.Tx, metadat
|
|||||||
// metadata 同时保留 coin_spent 和 charge_amount;投影使用最终收费金额作为礼物价值。
|
// metadata 同时保留 coin_spent 和 charge_amount;投影使用最终收费金额作为礼物价值。
|
||||||
chargeAmount = metadata.CoinSpent
|
chargeAmount = metadata.CoinSpent
|
||||||
}
|
}
|
||||||
totalCoinValue, totalDiamondValue := int64(0), int64(0)
|
totalCoinValue, totalDiamondValue := chargeAmount, int64(0)
|
||||||
if chargeAssetType == ledger.AssetDiamond {
|
|
||||||
totalDiamondValue = chargeAmount
|
|
||||||
} else {
|
|
||||||
totalCoinValue = chargeAmount
|
|
||||||
}
|
|
||||||
resourceSnapshotJSON := strings.TrimSpace(metadata.ResourceSnapshot)
|
resourceSnapshotJSON := strings.TrimSpace(metadata.ResourceSnapshot)
|
||||||
if resourceSnapshotJSON == "" {
|
if resourceSnapshotJSON == "" {
|
||||||
resourceSnapshotJSON = "{}"
|
resourceSnapshotJSON = "{}"
|
||||||
@ -1130,6 +1173,69 @@ func (r *Repository) resolveGiftPrice(ctx context.Context, tx *sql.Tx, giftID st
|
|||||||
return price, nil
|
return price, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type giftDiamondRatioSnapshot struct {
|
||||||
|
Percent string
|
||||||
|
PPM int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) resolveGiftDiamondRatio(ctx context.Context, tx *sql.Tx, appCode string, senderRegionID int64, giftTypeCode string) (giftDiamondRatioSnapshot, int64, error) {
|
||||||
|
giftTypeCode = strings.TrimSpace(giftTypeCode)
|
||||||
|
if giftTypeCode == "" {
|
||||||
|
giftTypeCode = "normal"
|
||||||
|
}
|
||||||
|
regions := []int64{senderRegionID}
|
||||||
|
if senderRegionID != 0 {
|
||||||
|
regions = append(regions, 0)
|
||||||
|
}
|
||||||
|
for _, regionID := range regions {
|
||||||
|
ratio, exists, err := r.getGiftDiamondRatio(ctx, tx, appCode, regionID, giftTypeCode)
|
||||||
|
if err != nil {
|
||||||
|
return giftDiamondRatioSnapshot{}, 0, err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return ratio, regionID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return giftDiamondRatioSnapshot{Percent: "100.00", PPM: 1_000_000}, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) getGiftDiamondRatio(ctx context.Context, tx *sql.Tx, appCode string, regionID int64, giftTypeCode string) (giftDiamondRatioSnapshot, bool, error) {
|
||||||
|
var percent string
|
||||||
|
var ppm int64
|
||||||
|
err := tx.QueryRowContext(ctx, `
|
||||||
|
SELECT CAST(ratio_percent AS CHAR), CAST(ROUND(ratio_percent * 10000) AS SIGNED)
|
||||||
|
FROM gift_diamond_ratio_configs
|
||||||
|
WHERE app_code = ? AND region_id = ? AND gift_type_code = ? AND status = 'active'
|
||||||
|
LIMIT 1`,
|
||||||
|
appcode.Normalize(appCode), regionID, giftTypeCode,
|
||||||
|
).Scan(&percent, &ppm)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return giftDiamondRatioSnapshot{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return giftDiamondRatioSnapshot{}, false, err
|
||||||
|
}
|
||||||
|
if ppm < 0 || ppm > 1_000_000 {
|
||||||
|
return giftDiamondRatioSnapshot{}, false, xerr.New(xerr.InvalidArgument, "gift diamond ratio is invalid")
|
||||||
|
}
|
||||||
|
percent = strings.TrimSpace(percent)
|
||||||
|
if percent == "" {
|
||||||
|
percent = "100.00"
|
||||||
|
}
|
||||||
|
return giftDiamondRatioSnapshot{Percent: percent, PPM: ppm}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func giftDiamondAmount(chargeAmount int64, ratioPPM int64) (int64, error) {
|
||||||
|
if chargeAmount < 0 || ratioPPM < 0 {
|
||||||
|
return 0, xerr.New(xerr.InvalidArgument, "gift diamond amount is invalid")
|
||||||
|
}
|
||||||
|
product, err := checkedMul(chargeAmount, ratioPPM)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return product / 1_000_000, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) resolveRechargePolicy(ctx context.Context, tx *sql.Tx, regionID int64, nowMs int64) (rechargePolicy, error) {
|
func (r *Repository) resolveRechargePolicy(ctx context.Context, tx *sql.Tx, regionID int64, nowMs int64) (rechargePolicy, error) {
|
||||||
row := tx.QueryRowContext(ctx,
|
row := tx.QueryRowContext(ctx,
|
||||||
`SELECT policy_id, region_id, policy_version, currency_code, coin_amount, usd_minor_amount, effective_from_ms
|
`SELECT policy_id, region_id, policy_version, currency_code, coin_amount, usd_minor_amount, effective_from_ms
|
||||||
@ -1800,11 +1906,14 @@ type giftMetadata struct {
|
|||||||
BalanceAfter int64 `json:"balance_after"`
|
BalanceAfter int64 `json:"balance_after"`
|
||||||
BillingReceipt string `json:"billing_receipt_id"`
|
BillingReceipt string `json:"billing_receipt_id"`
|
||||||
SenderUserID int64 `json:"sender_user_id"`
|
SenderUserID int64 `json:"sender_user_id"`
|
||||||
|
SenderRegionID int64 `json:"sender_region_id"`
|
||||||
TargetUserID int64 `json:"target_user_id"`
|
TargetUserID int64 `json:"target_user_id"`
|
||||||
TargetIsHost bool `json:"target_is_host"`
|
TargetIsHost bool `json:"target_is_host"`
|
||||||
TargetHostRegionID int64 `json:"target_host_region_id"`
|
TargetHostRegionID int64 `json:"target_host_region_id"`
|
||||||
TargetAgencyOwnerUserID int64 `json:"target_agency_owner_user_id"`
|
TargetAgencyOwnerUserID int64 `json:"target_agency_owner_user_id"`
|
||||||
HostPeriodDiamondAdded int64 `json:"host_period_diamond_added"`
|
HostPeriodDiamondAdded int64 `json:"host_period_diamond_added"`
|
||||||
|
GiftDiamondRatioPercent string `json:"gift_diamond_ratio_percent"`
|
||||||
|
GiftDiamondRatioRegionID int64 `json:"gift_diamond_ratio_region_id"`
|
||||||
HostPeriodDiamondAfter int64 `json:"host_period_diamond_after"`
|
HostPeriodDiamondAfter int64 `json:"host_period_diamond_after"`
|
||||||
HostPeriodDiamondVersion int64 `json:"host_period_diamond_version"`
|
HostPeriodDiamondVersion int64 `json:"host_period_diamond_version"`
|
||||||
HostPeriodCycleKey string `json:"host_period_cycle_key"`
|
HostPeriodCycleKey string `json:"host_period_cycle_key"`
|
||||||
@ -2255,22 +2364,24 @@ func hostPeriodDiamondCreditedEvent(transactionID string, commandID string, meta
|
|||||||
AvailableDelta: metadata.HostPeriodDiamondAdded,
|
AvailableDelta: metadata.HostPeriodDiamondAdded,
|
||||||
FrozenDelta: 0,
|
FrozenDelta: 0,
|
||||||
Payload: map[string]any{
|
Payload: map[string]any{
|
||||||
"transaction_id": transactionID,
|
"transaction_id": transactionID,
|
||||||
"command_id": commandID,
|
"command_id": commandID,
|
||||||
"host_user_id": metadata.TargetUserID,
|
"host_user_id": metadata.TargetUserID,
|
||||||
"cycle_key": metadata.HostPeriodCycleKey,
|
"cycle_key": metadata.HostPeriodCycleKey,
|
||||||
"region_id": metadata.TargetHostRegionID,
|
"region_id": metadata.TargetHostRegionID,
|
||||||
"agency_owner_user_id": metadata.TargetAgencyOwnerUserID,
|
"agency_owner_user_id": metadata.TargetAgencyOwnerUserID,
|
||||||
"diamond_delta": metadata.HostPeriodDiamondAdded,
|
"diamond_delta": metadata.HostPeriodDiamondAdded,
|
||||||
"diamond_after": metadata.HostPeriodDiamondAfter,
|
"diamond_after": metadata.HostPeriodDiamondAfter,
|
||||||
"gift_charge_asset_type": metadata.ChargeAssetType,
|
"gift_charge_asset_type": metadata.ChargeAssetType,
|
||||||
"gift_charge_amount": metadata.ChargeAmount,
|
"gift_charge_amount": metadata.ChargeAmount,
|
||||||
"gift_id": metadata.GiftID,
|
"gift_id": metadata.GiftID,
|
||||||
"gift_count": metadata.GiftCount,
|
"gift_count": metadata.GiftCount,
|
||||||
"sender_user_id": metadata.SenderUserID,
|
"sender_user_id": metadata.SenderUserID,
|
||||||
"room_id": metadata.RoomID,
|
"sender_region_id": metadata.SenderRegionID,
|
||||||
"conversion_ratio_snapshot": "1",
|
"room_id": metadata.RoomID,
|
||||||
"created_at_ms": nowMs,
|
"gift_diamond_ratio_percent": metadata.GiftDiamondRatioPercent,
|
||||||
|
"gift_diamond_ratio_region_id": metadata.GiftDiamondRatioRegionID,
|
||||||
|
"created_at_ms": nowMs,
|
||||||
},
|
},
|
||||||
CreatedAtMS: nowMs,
|
CreatedAtMS: nowMs,
|
||||||
}
|
}
|
||||||
@ -2330,7 +2441,7 @@ func hostPeriodCycleKeyFromMS(ms int64) string {
|
|||||||
|
|
||||||
func debitRequestHash(command ledger.DebitGiftCommand) string {
|
func debitRequestHash(command ledger.DebitGiftCommand) string {
|
||||||
// 哈希覆盖所有账务语义字段;相同 command_id 只能重放完全相同的业务命令。
|
// 哈希覆盖所有账务语义字段;相同 command_id 只能重放完全相同的业务命令。
|
||||||
return stableHash(fmt.Sprintf("gift|%s|%s|%d|%d|%s|%d|%s|%d|%t|%d|%d",
|
return stableHash(fmt.Sprintf("gift|%s|%s|%d|%d|%s|%d|%s|%d|%d|%t|%d|%d",
|
||||||
appcode.Normalize(command.AppCode),
|
appcode.Normalize(command.AppCode),
|
||||||
command.RoomID,
|
command.RoomID,
|
||||||
command.SenderUserID,
|
command.SenderUserID,
|
||||||
@ -2339,6 +2450,7 @@ func debitRequestHash(command ledger.DebitGiftCommand) string {
|
|||||||
command.GiftCount,
|
command.GiftCount,
|
||||||
strings.TrimSpace(command.PriceVersion),
|
strings.TrimSpace(command.PriceVersion),
|
||||||
command.RegionID,
|
command.RegionID,
|
||||||
|
command.SenderRegionID,
|
||||||
command.TargetIsHost,
|
command.TargetIsHost,
|
||||||
command.TargetHostRegionID,
|
command.TargetHostRegionID,
|
||||||
command.TargetAgencyOwnerUserID,
|
command.TargetAgencyOwnerUserID,
|
||||||
|
|||||||
@ -2948,7 +2948,7 @@ func normalizeResourceGroupItemInput(item resourcedomain.ResourceGroupItemInput)
|
|||||||
|
|
||||||
func validGroupWalletAssetType(assetType string) bool {
|
func validGroupWalletAssetType(assetType string) bool {
|
||||||
switch resourcedomain.NormalizeWalletAssetType(assetType) {
|
switch resourcedomain.NormalizeWalletAssetType(assetType) {
|
||||||
case ledger.AssetCoin, ledger.AssetDiamond:
|
case ledger.AssetCoin:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -167,6 +167,20 @@ func (r *Repository) MoveHostPeriodCycle(userID int64, fromCycle string, toCycle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostPeriodGiftDiamondTotal returns the current accumulated gift diamonds for a host.
|
||||||
|
func (r *Repository) HostPeriodGiftDiamondTotal(userID int64) int64 {
|
||||||
|
r.t.Helper()
|
||||||
|
var total int64
|
||||||
|
err := r.schema.DB.QueryRowContext(context.Background(), `
|
||||||
|
SELECT COALESCE(SUM(gift_diamond_total), 0)
|
||||||
|
FROM host_period_diamond_accounts
|
||||||
|
WHERE user_id = ?`, userID).Scan(&total)
|
||||||
|
if err != nil {
|
||||||
|
r.t.Fatalf("query host period gift diamond total failed: %v", err)
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
// InsertRawGiftPrice allows tests to seed inactive or future prices without service helpers.
|
// InsertRawGiftPrice allows tests to seed inactive or future prices without service helpers.
|
||||||
func (r *Repository) InsertRawGiftPrice(giftID string, priceVersion string, status string, effectiveAtMs int64) {
|
func (r *Repository) InsertRawGiftPrice(giftID string, priceVersion string, status string, effectiveAtMs int64) {
|
||||||
r.t.Helper()
|
r.t.Helper()
|
||||||
@ -370,6 +384,37 @@ func (r *Repository) SetGiftPrice(giftID string, priceVersion string, coinPrice
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetGiftType updates a seeded gift config to the requested gift type.
|
||||||
|
func (r *Repository) SetGiftType(giftID string, giftTypeCode string) {
|
||||||
|
r.t.Helper()
|
||||||
|
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
|
UPDATE gift_configs
|
||||||
|
SET gift_type_code = ?, updated_at_ms = ?
|
||||||
|
WHERE gift_id = ?`, giftTypeCode, time.Now().UnixMilli(), giftID)
|
||||||
|
if err != nil {
|
||||||
|
r.t.Fatalf("set gift type failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGiftDiamondRatio configures the host-period diamond ratio for a sender region and gift type.
|
||||||
|
func (r *Repository) SetGiftDiamondRatio(regionID int64, giftTypeCode string, ratioPercent string) {
|
||||||
|
r.t.Helper()
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
|
INSERT INTO gift_diamond_ratio_configs (
|
||||||
|
region_id, gift_type_code, status, ratio_percent,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, 'active', ?, 0, 0, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
status = VALUES(status),
|
||||||
|
ratio_percent = VALUES(ratio_percent),
|
||||||
|
updated_at_ms = VALUES(updated_at_ms)`,
|
||||||
|
regionID, giftTypeCode, ratioPercent, nowMs, nowMs)
|
||||||
|
if err != nil {
|
||||||
|
r.t.Fatalf("set gift diamond ratio failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetRechargePolicy 配置区域充值汇率,用于币商转账充值口径测试。
|
// SetRechargePolicy 配置区域充值汇率,用于币商转账充值口径测试。
|
||||||
func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coinAmount int64, usdMinorAmount int64) {
|
func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coinAmount int64, usdMinorAmount int64) {
|
||||||
r.t.Helper()
|
r.t.Helper()
|
||||||
@ -489,7 +534,7 @@ func validateTableName(table string) string {
|
|||||||
case "wallet_transactions", "wallet_entries", "wallet_outbox", "wallet_accounts", "wallet_recharge_records",
|
case "wallet_transactions", "wallet_entries", "wallet_outbox", "wallet_accounts", "wallet_recharge_records",
|
||||||
"host_period_diamond_accounts", "host_period_diamond_entries", "host_agency_salary_policies",
|
"host_period_diamond_accounts", "host_period_diamond_entries", "host_agency_salary_policies",
|
||||||
"host_agency_salary_policy_levels", "host_salary_settlement_progress", "host_salary_settlement_records",
|
"host_agency_salary_policy_levels", "host_salary_settlement_progress", "host_salary_settlement_records",
|
||||||
"coin_seller_stock_records", "wallet_diamond_exchange_rules", "wallet_withdrawal_requests",
|
"coin_seller_stock_records", "gift_diamond_ratio_configs", "wallet_diamond_exchange_rules", "wallet_withdrawal_requests",
|
||||||
"vip_levels", "user_vip_memberships", "vip_purchase_orders", "user_vip_history",
|
"vip_levels", "user_vip_memberships", "vip_purchase_orders", "user_vip_history",
|
||||||
"resources", "resource_groups", "resource_group_items", "resource_shop_items", "resource_shop_purchase_orders", "gift_type_configs", "gift_configs", "gift_config_regions", "user_gift_wall", "wallet_projection_events",
|
"resources", "resource_groups", "resource_group_items", "resource_shop_items", "resource_shop_purchase_orders", "gift_type_configs", "gift_configs", "gift_config_regions", "user_gift_wall", "wallet_projection_events",
|
||||||
"resource_grants", "resource_grant_items", "user_resource_entitlements", "user_resource_equipment":
|
"resource_grants", "resource_grant_items", "user_resource_entitlements", "user_resource_equipment":
|
||||||
|
|||||||
@ -27,16 +27,17 @@ func NewServer(svc *walletservice.Service) *Server {
|
|||||||
func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
receipt, err := s.svc.DebitGift(ctx, ledger.DebitGiftCommand{
|
receipt, err := s.svc.DebitGift(ctx, ledger.DebitGiftCommand{
|
||||||
AppCode: req.GetAppCode(),
|
AppCode: req.GetAppCode(),
|
||||||
CommandID: req.GetCommandId(),
|
CommandID: req.GetCommandId(),
|
||||||
RoomID: req.GetRoomId(),
|
RoomID: req.GetRoomId(),
|
||||||
SenderUserID: req.GetSenderUserId(),
|
SenderUserID: req.GetSenderUserId(),
|
||||||
TargetUserID: req.GetTargetUserId(),
|
TargetUserID: req.GetTargetUserId(),
|
||||||
GiftID: req.GetGiftId(),
|
GiftID: req.GetGiftId(),
|
||||||
GiftCount: req.GetGiftCount(),
|
GiftCount: req.GetGiftCount(),
|
||||||
PriceVersion: req.GetPriceVersion(),
|
PriceVersion: req.GetPriceVersion(),
|
||||||
RegionID: req.GetRegionId(),
|
RegionID: req.GetRegionId(),
|
||||||
TargetIsHost: req.GetTargetIsHost(),
|
SenderRegionID: req.GetSenderRegionId(),
|
||||||
|
TargetIsHost: req.GetTargetIsHost(),
|
||||||
// 工资政策区域来自 user-service host profile,不能用房间 visible_region_id 兜底。
|
// 工资政策区域来自 user-service host profile,不能用房间 visible_region_id 兜底。
|
||||||
TargetHostRegionID: req.GetTargetHostRegionId(),
|
TargetHostRegionID: req.GetTargetHostRegionId(),
|
||||||
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
|
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user