feat: 接入论坛奖励 outbox 链路
This commit is contained in:
@@ -3,6 +3,7 @@ package dao
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox"
|
||||
tokenmodel "github.com/LoveLosita/smartflow/backend/services/tokenstore/model"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/driver/mysql"
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
// OpenDBFromConfig 创建 token-store 服务自己的数据库句柄,并迁移本服务私有表。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只迁移 token_* 表,不迁移 users,避免和 user/auth 服务边界冲突;
|
||||
// 1. 只迁移 token_* 表和 token-store outbox 表,不迁移 users,避免和 user/auth 服务边界冲突;
|
||||
// 2. 自动迁移后执行 P0 seed,确保前端商品页有可展示商品;
|
||||
// 3. 返回 *gorm.DB 供本服务 DAO 复用,调用方负责进程生命周期。
|
||||
func OpenDBFromConfig() (*gorm.DB, error) {
|
||||
@@ -45,8 +46,9 @@ func OpenDBFromConfig() (*gorm.DB, error) {
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 先创建商品、订单、获取账本和奖励规则表;
|
||||
// 2. 通过唯一约束保证 order_no、event_id 和幂等键不会重复写入;
|
||||
// 3. 失败时直接返回错误,避免服务在 schema 不完整时继续启动。
|
||||
// 2. 再按 service catalog 创建 token-store outbox 表,保证论坛奖励事件有稳定落表目录;
|
||||
// 3. 通过唯一约束保证 order_no、event_id 和幂等键不会重复写入;
|
||||
// 4. 失败时直接返回错误,避免服务在 schema 不完整时继续启动。
|
||||
func AutoMigrate(db *gorm.DB) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("tokenstore auto migrate failed: db is nil")
|
||||
@@ -59,6 +61,9 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
); err != nil {
|
||||
return fmt.Errorf("auto migrate tokenstore tables failed: %w", err)
|
||||
}
|
||||
if err := outboxinfra.AutoMigrateServiceTable(db, outboxinfra.ServiceTokenStore); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -172,7 +177,7 @@ func defaultTokenRewardRules() []tokenmodel.TokenRewardRule {
|
||||
{
|
||||
Source: tokenmodel.TokenGrantSourceForumImport,
|
||||
Name: "计划被导入奖励",
|
||||
Amount: 2,
|
||||
Amount: 5,
|
||||
Status: tokenmodel.TokenRewardRuleStatusActive,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,6 +64,31 @@ func (dao *TokenStoreDAO) ListActiveProducts(ctx context.Context) ([]tokenmodel.
|
||||
return products, err
|
||||
}
|
||||
|
||||
// FindRewardRuleBySource 按来源读取社区奖励规则。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只读取 token_reward_rules,不计算最终发放金额,也不判断停用语义;
|
||||
// 2. 未找到规则时返回 nil,由服务层决定配置或默认值兜底;
|
||||
// 3. source 在 DAO 层做一次规范化,避免大小写和空格造成规则漏命中。
|
||||
func (dao *TokenStoreDAO) FindRewardRuleBySource(ctx context.Context, source string) (*tokenmodel.TokenRewardRule, error) {
|
||||
source = strings.ToLower(strings.TrimSpace(source))
|
||||
if source == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var rule tokenmodel.TokenRewardRule
|
||||
err := dao.db.WithContext(ctx).
|
||||
Where("source = ?", source).
|
||||
First(&rule).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
func (dao *TokenStoreDAO) FindActiveProductByID(ctx context.Context, productID uint64) (*tokenmodel.TokenProduct, error) {
|
||||
var product tokenmodel.TokenProduct
|
||||
err := dao.db.WithContext(ctx).
|
||||
|
||||
@@ -171,6 +171,28 @@ func (h *Handler) ListGrants(ctx context.Context, req *pb.ListTokenGrantsRequest
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RecordForumRewardGrant 负责把论坛 outbox 奖励事件转成 token-store 内部账本写入调用。
|
||||
func (h *Handler) RecordForumRewardGrant(ctx context.Context, req *pb.RecordForumRewardGrantRequest) (*pb.RecordForumRewardGrantResponse, error) {
|
||||
svc, err := h.service()
|
||||
if err != nil {
|
||||
return nil, grpcErrorFromServiceError(err)
|
||||
}
|
||||
if req == nil {
|
||||
return nil, grpcErrorFromServiceError(respond.MissingParam)
|
||||
}
|
||||
|
||||
grant, err := svc.RecordForumRewardGrant(ctx, tokencontracts.RecordForumRewardGrantRequest{
|
||||
EventID: req.EventId,
|
||||
ReceiverUserID: req.ReceiverUserId,
|
||||
Source: req.Source,
|
||||
SourceRefID: req.SourceRefId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, grpcErrorFromServiceError(err)
|
||||
}
|
||||
return &pb.RecordForumRewardGrantResponse{Grant: tokenGrantToPB(grant)}, nil
|
||||
}
|
||||
|
||||
func tokenPageToPB(page tokencontracts.PageResult) *pb.PageResponse {
|
||||
return &pb.PageResponse{
|
||||
Page: int32(page.Page),
|
||||
|
||||
@@ -210,3 +210,22 @@ type ListTokenGrantsResponse struct {
|
||||
func (m *ListTokenGrantsResponse) Reset() { *m = ListTokenGrantsResponse{} }
|
||||
func (m *ListTokenGrantsResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ListTokenGrantsResponse) ProtoMessage() {}
|
||||
|
||||
type RecordForumRewardGrantRequest struct {
|
||||
EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"`
|
||||
ReceiverUserId uint64 `protobuf:"varint,2,opt,name=receiver_user_id,json=receiverUserId,proto3" json:"receiver_user_id,omitempty"`
|
||||
Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"`
|
||||
SourceRefId string `protobuf:"bytes,4,opt,name=source_ref_id,json=sourceRefId,proto3" json:"source_ref_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *RecordForumRewardGrantRequest) Reset() { *m = RecordForumRewardGrantRequest{} }
|
||||
func (m *RecordForumRewardGrantRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*RecordForumRewardGrantRequest) ProtoMessage() {}
|
||||
|
||||
type RecordForumRewardGrantResponse struct {
|
||||
Grant *TokenGrantView `protobuf:"bytes,1,opt,name=grant,proto3" json:"grant,omitempty"`
|
||||
}
|
||||
|
||||
func (m *RecordForumRewardGrantResponse) Reset() { *m = RecordForumRewardGrantResponse{} }
|
||||
func (m *RecordForumRewardGrantResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*RecordForumRewardGrantResponse) ProtoMessage() {}
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TokenStoreService_GetSummary_FullMethodName = "/smartflow.tokenstore.TokenStoreService/GetSummary"
|
||||
TokenStoreService_ListProducts_FullMethodName = "/smartflow.tokenstore.TokenStoreService/ListProducts"
|
||||
TokenStoreService_CreateOrder_FullMethodName = "/smartflow.tokenstore.TokenStoreService/CreateOrder"
|
||||
TokenStoreService_ListOrders_FullMethodName = "/smartflow.tokenstore.TokenStoreService/ListOrders"
|
||||
TokenStoreService_GetOrder_FullMethodName = "/smartflow.tokenstore.TokenStoreService/GetOrder"
|
||||
TokenStoreService_MockPaidOrder_FullMethodName = "/smartflow.tokenstore.TokenStoreService/MockPaidOrder"
|
||||
TokenStoreService_ListGrants_FullMethodName = "/smartflow.tokenstore.TokenStoreService/ListGrants"
|
||||
TokenStoreService_GetSummary_FullMethodName = "/smartflow.tokenstore.TokenStoreService/GetSummary"
|
||||
TokenStoreService_ListProducts_FullMethodName = "/smartflow.tokenstore.TokenStoreService/ListProducts"
|
||||
TokenStoreService_CreateOrder_FullMethodName = "/smartflow.tokenstore.TokenStoreService/CreateOrder"
|
||||
TokenStoreService_ListOrders_FullMethodName = "/smartflow.tokenstore.TokenStoreService/ListOrders"
|
||||
TokenStoreService_GetOrder_FullMethodName = "/smartflow.tokenstore.TokenStoreService/GetOrder"
|
||||
TokenStoreService_MockPaidOrder_FullMethodName = "/smartflow.tokenstore.TokenStoreService/MockPaidOrder"
|
||||
TokenStoreService_ListGrants_FullMethodName = "/smartflow.tokenstore.TokenStoreService/ListGrants"
|
||||
TokenStoreService_RecordForumRewardGrant_FullMethodName = "/smartflow.tokenstore.TokenStoreService/RecordForumRewardGrant"
|
||||
)
|
||||
|
||||
type TokenStoreServiceClient interface {
|
||||
@@ -26,6 +27,7 @@ type TokenStoreServiceClient interface {
|
||||
GetOrder(ctx context.Context, in *GetTokenOrderRequest, opts ...grpc.CallOption) (*GetTokenOrderResponse, error)
|
||||
MockPaidOrder(ctx context.Context, in *MockPaidOrderRequest, opts ...grpc.CallOption) (*MockPaidOrderResponse, error)
|
||||
ListGrants(ctx context.Context, in *ListTokenGrantsRequest, opts ...grpc.CallOption) (*ListTokenGrantsResponse, error)
|
||||
RecordForumRewardGrant(ctx context.Context, in *RecordForumRewardGrantRequest, opts ...grpc.CallOption) (*RecordForumRewardGrantResponse, error)
|
||||
}
|
||||
|
||||
type tokenStoreServiceClient struct {
|
||||
@@ -64,6 +66,10 @@ func (c *tokenStoreServiceClient) ListGrants(ctx context.Context, in *ListTokenG
|
||||
return invokeTokenStore[ListTokenGrantsResponse](ctx, c.cc, TokenStoreService_ListGrants_FullMethodName, in, opts...)
|
||||
}
|
||||
|
||||
func (c *tokenStoreServiceClient) RecordForumRewardGrant(ctx context.Context, in *RecordForumRewardGrantRequest, opts ...grpc.CallOption) (*RecordForumRewardGrantResponse, error) {
|
||||
return invokeTokenStore[RecordForumRewardGrantResponse](ctx, c.cc, TokenStoreService_RecordForumRewardGrant_FullMethodName, in, opts...)
|
||||
}
|
||||
|
||||
func invokeTokenStore[Resp any](ctx context.Context, cc grpc.ClientConnInterface, fullMethod string, in interface{}, opts ...grpc.CallOption) (*Resp, error) {
|
||||
out := new(Resp)
|
||||
err := cc.Invoke(ctx, fullMethod, in, out, opts...)
|
||||
@@ -81,6 +87,7 @@ type TokenStoreServiceServer interface {
|
||||
GetOrder(context.Context, *GetTokenOrderRequest) (*GetTokenOrderResponse, error)
|
||||
MockPaidOrder(context.Context, *MockPaidOrderRequest) (*MockPaidOrderResponse, error)
|
||||
ListGrants(context.Context, *ListTokenGrantsRequest) (*ListTokenGrantsResponse, error)
|
||||
RecordForumRewardGrant(context.Context, *RecordForumRewardGrantRequest) (*RecordForumRewardGrantResponse, error)
|
||||
}
|
||||
|
||||
type UnimplementedTokenStoreServiceServer struct{}
|
||||
@@ -113,6 +120,10 @@ func (UnimplementedTokenStoreServiceServer) ListGrants(context.Context, *ListTok
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGrants not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedTokenStoreServiceServer) RecordForumRewardGrant(context.Context, *RecordForumRewardGrantRequest) (*RecordForumRewardGrantResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RecordForumRewardGrant not implemented")
|
||||
}
|
||||
|
||||
func RegisterTokenStoreServiceServer(s grpc.ServiceRegistrar, srv TokenStoreServiceServer) {
|
||||
s.RegisterService(&TokenStoreService_ServiceDesc, srv)
|
||||
}
|
||||
@@ -165,6 +176,9 @@ var TokenStoreService_ServiceDesc = grpc.ServiceDesc{
|
||||
tokenStoreUnaryHandler[ListTokenGrantsRequest]("ListGrants", TokenStoreService_ListGrants_FullMethodName, func(s TokenStoreServiceServer, ctx context.Context, req *ListTokenGrantsRequest) (interface{}, error) {
|
||||
return s.ListGrants(ctx, req)
|
||||
}),
|
||||
tokenStoreUnaryHandler[RecordForumRewardGrantRequest]("RecordForumRewardGrant", TokenStoreService_RecordForumRewardGrant_FullMethodName, func(s TokenStoreServiceServer, ctx context.Context, req *RecordForumRewardGrantRequest) (interface{}, error) {
|
||||
return s.RecordForumRewardGrant(ctx, req)
|
||||
}),
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "tokenstore.proto",
|
||||
|
||||
@@ -12,6 +12,7 @@ service TokenStoreService {
|
||||
rpc GetOrder(GetTokenOrderRequest) returns (GetTokenOrderResponse);
|
||||
rpc MockPaidOrder(MockPaidOrderRequest) returns (MockPaidOrderResponse);
|
||||
rpc ListGrants(ListTokenGrantsRequest) returns (ListTokenGrantsResponse);
|
||||
rpc RecordForumRewardGrant(RecordForumRewardGrantRequest) returns (RecordForumRewardGrantResponse);
|
||||
}
|
||||
|
||||
message PageResponse {
|
||||
@@ -142,3 +143,14 @@ message ListTokenGrantsResponse {
|
||||
repeated TokenGrantView items = 1;
|
||||
PageResponse page = 2;
|
||||
}
|
||||
|
||||
message RecordForumRewardGrantRequest {
|
||||
string event_id = 1;
|
||||
uint64 receiver_user_id = 2;
|
||||
string source = 3;
|
||||
string source_ref_id = 4;
|
||||
}
|
||||
|
||||
message RecordForumRewardGrantResponse {
|
||||
TokenGrantView grant = 1;
|
||||
}
|
||||
|
||||
234
backend/services/tokenstore/sv/reward.go
Normal file
234
backend/services/tokenstore/sv/reward.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package sv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tokenmodel "github.com/LoveLosita/smartflow/backend/services/tokenstore/model"
|
||||
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
forumLikeRewardConfigKey = "tokenstore.reward.forumLikeAmount"
|
||||
forumImportRewardConfigKey = "tokenstore.reward.forumImportAmount"
|
||||
|
||||
defaultForumLikeRewardAmount int64 = 1
|
||||
defaultForumImportRewardAmount int64 = 5
|
||||
)
|
||||
|
||||
type forumRewardGrantRequest struct {
|
||||
EventID string
|
||||
ReceiverUserID uint64
|
||||
Source string
|
||||
SourceRefID uint64
|
||||
}
|
||||
|
||||
type forumRewardDecision struct {
|
||||
Amount int64
|
||||
Status string
|
||||
Description string
|
||||
}
|
||||
|
||||
// RecordForumRewardGrant 负责把论坛点赞/导入奖励写入 token_grants。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只处理 forum_like / forum_import 两类奖励账本写入,不修改 users,也不调用 user/auth;
|
||||
// 2. 以 event_id 作为最终幂等边界,重复请求校验一致后返回既有 grant;
|
||||
// 3. 奖励金额优先读取 token_reward_rules,配置和代码默认值只作为兜底。
|
||||
func (s *Service) RecordForumRewardGrant(ctx context.Context, req tokencontracts.RecordForumRewardGrantRequest) (*tokencontracts.TokenGrantView, error) {
|
||||
if err := s.Ready(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
normalized, err := normalizeForumRewardGrantRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. 先按 event_id 回查,命中时直接视为成功,避免 outbox 重试重复写账本。
|
||||
// 2. 命中后必须校验用户、来源和来源业务 ID,避免错误复用 event_id 时静默吞掉错账。
|
||||
// 3. 校验通过才返回既有 grant,兼容“首次已成功、调用方超时后重试”的常见场景。
|
||||
existing, err := s.tokenDAO.FindGrantByEventID(ctx, normalized.EventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil {
|
||||
if err := validateExistingForumRewardGrant(*existing, normalized); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view := grantViewFromModel(*existing)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
sourceRefID := normalized.SourceRefID
|
||||
decision, err := s.forumRewardDecision(ctx, normalized.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grant := tokenmodel.TokenGrant{
|
||||
EventID: normalized.EventID,
|
||||
UserID: normalized.ReceiverUserID,
|
||||
Source: normalized.Source,
|
||||
SourceLabel: grantSourceLabel(normalized.Source, ""),
|
||||
SourceRefID: &sourceRefID,
|
||||
Amount: decision.Amount,
|
||||
Status: decision.Status,
|
||||
QuotaApplied: false,
|
||||
Description: decision.Description,
|
||||
}
|
||||
|
||||
// 1. 账本写入只依赖 token_grants.event_id 唯一约束兜底并发幂等。
|
||||
// 2. 若并发下插入触发唯一键冲突,立刻回查 event_id,把已有 grant 当作成功结果返回。
|
||||
// 3. 只有“冲突后仍查不到旧记录”这种异常态才上抛内部错误,避免吞掉真实一致性问题。
|
||||
if err := s.tokenDAO.CreateGrant(ctx, &grant); err != nil {
|
||||
if !isDuplicateKeyError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := s.tokenDAO.FindGrantByEventID(ctx, normalized.EventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing == nil {
|
||||
return nil, errors.New("forum reward grant duplicated but not found by event_id")
|
||||
}
|
||||
if err := validateExistingForumRewardGrant(*existing, normalized); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view := grantViewFromModel(*existing)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
view := grantViewFromModel(grant)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func normalizeForumRewardGrantRequest(req tokencontracts.RecordForumRewardGrantRequest) (forumRewardGrantRequest, error) {
|
||||
normalized := forumRewardGrantRequest{
|
||||
EventID: strings.TrimSpace(req.EventID),
|
||||
ReceiverUserID: req.ReceiverUserID,
|
||||
Source: strings.ToLower(strings.TrimSpace(req.Source)),
|
||||
}
|
||||
|
||||
switch {
|
||||
case normalized.EventID == "":
|
||||
return forumRewardGrantRequest{}, tokenStoreBadRequest("event_id 不能为空")
|
||||
case normalized.ReceiverUserID == 0:
|
||||
return forumRewardGrantRequest{}, tokenStoreBadRequest("receiver_user_id 不能为空")
|
||||
}
|
||||
|
||||
sourceRefID, err := parseForumRewardSourceRefID(req.SourceRefID)
|
||||
if err != nil {
|
||||
return forumRewardGrantRequest{}, err
|
||||
}
|
||||
normalized.SourceRefID = sourceRefID
|
||||
|
||||
switch normalized.Source {
|
||||
case tokenmodel.TokenGrantSourceForumLike, tokenmodel.TokenGrantSourceForumImport:
|
||||
return normalized, nil
|
||||
default:
|
||||
return forumRewardGrantRequest{}, tokenStoreBadRequest("source 仅支持 forum_like 或 forum_import")
|
||||
}
|
||||
}
|
||||
|
||||
func parseForumRewardSourceRefID(raw string) (uint64, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return 0, tokenStoreBadRequest("source_ref_id 不能为空")
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
||||
if err != nil || parsed == 0 {
|
||||
return 0, tokenStoreBadRequest("source_ref_id 必须是正整数")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// validateExistingForumRewardGrant 校验重复 event_id 是否真的是同一条论坛奖励。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只比较幂等所需的最小字段:接收人、来源和来源业务 ID;
|
||||
// 2. 不比较金额和状态,避免规则调整后重放旧事件被误判;
|
||||
// 3. 不一致时返回业务校验错误,让上游暴露这类错账风险。
|
||||
func validateExistingForumRewardGrant(existing tokenmodel.TokenGrant, req forumRewardGrantRequest) error {
|
||||
sourceRefID := uint64(0)
|
||||
if existing.SourceRefID != nil {
|
||||
sourceRefID = *existing.SourceRefID
|
||||
}
|
||||
if existing.UserID != req.ReceiverUserID || existing.Source != req.Source || sourceRefID != req.SourceRefID {
|
||||
return tokenStoreBadRequest("event_id 幂等冲突:已有奖励记录与本次论坛奖励请求不一致")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// forumRewardDecision 解析论坛奖励发放决策。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 优先读取 token_reward_rules,保持“从表里读”的 P0 口径;
|
||||
// 2. 规则停用或金额非正时写 skipped 账本,消费 outbox 但不增加 Token;
|
||||
// 3. 表规则缺失时再读取配置和代码默认值,兼容旧环境尚未 seed 的情况。
|
||||
func (s *Service) forumRewardDecision(ctx context.Context, source string) (forumRewardDecision, error) {
|
||||
rule, err := s.tokenDAO.FindRewardRuleBySource(ctx, source)
|
||||
if err != nil {
|
||||
return forumRewardDecision{}, err
|
||||
}
|
||||
if rule != nil {
|
||||
if strings.TrimSpace(rule.Status) != tokenmodel.TokenRewardRuleStatusActive {
|
||||
return skippedForumRewardDecision(source, "奖励规则已停用,未发放 Token"), nil
|
||||
}
|
||||
if rule.Amount <= 0 {
|
||||
return skippedForumRewardDecision(source, "奖励规则金额非正,未发放 Token"), nil
|
||||
}
|
||||
return recordedForumRewardDecision(source, rule.Amount), nil
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(source) {
|
||||
case tokenmodel.TokenGrantSourceForumLike:
|
||||
return recordedForumRewardDecision(source, positiveConfigAmountOrDefault(forumLikeRewardConfigKey, defaultForumLikeRewardAmount)), nil
|
||||
case tokenmodel.TokenGrantSourceForumImport:
|
||||
return recordedForumRewardDecision(source, positiveConfigAmountOrDefault(forumImportRewardConfigKey, defaultForumImportRewardAmount)), nil
|
||||
default:
|
||||
return skippedForumRewardDecision(source, "未知论坛奖励来源,未发放 Token"), nil
|
||||
}
|
||||
}
|
||||
|
||||
func recordedForumRewardDecision(source string, amount int64) forumRewardDecision {
|
||||
if amount <= 0 {
|
||||
return skippedForumRewardDecision(source, "奖励金额非正,未发放 Token")
|
||||
}
|
||||
return forumRewardDecision{
|
||||
Amount: amount,
|
||||
Status: tokenmodel.TokenGrantStatusRecorded,
|
||||
Description: forumRewardDescription(source),
|
||||
}
|
||||
}
|
||||
|
||||
func skippedForumRewardDecision(source string, description string) forumRewardDecision {
|
||||
return forumRewardDecision{
|
||||
Amount: 0,
|
||||
Status: tokenmodel.TokenGrantStatusSkipped,
|
||||
Description: strings.TrimSpace(description),
|
||||
}
|
||||
}
|
||||
|
||||
func positiveConfigAmountOrDefault(configKey string, fallback int64) int64 {
|
||||
amount := viper.GetInt64(configKey)
|
||||
if amount <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
func forumRewardDescription(source string) string {
|
||||
switch strings.TrimSpace(source) {
|
||||
case tokenmodel.TokenGrantSourceForumLike:
|
||||
return "计划被点赞奖励"
|
||||
case tokenmodel.TokenGrantSourceForumImport:
|
||||
return "计划被导入奖励"
|
||||
default:
|
||||
return "论坛奖励入账"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user