Skip to content

Commit 69af419

Browse files
feat: 添加社区发帖活动及其相关模型和功能实现,包括数据库迁移脚本和Makefile
1 parent a4ae9c7 commit 69af419

10 files changed

+297
-68
lines changed

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# init project data
2+
GOOSE_CMD = goose -dir db/migrations mysql "root:root@tcp(localhost:3306)/db_q_goods_center?parseTime=true"
3+
4+
.PHONY: migrate-up
5+
migrate-up:
6+
$(GOOSE_CMD) up
7+
8+
.PHONY: migrate-down
9+
migrate-down:
10+
$(GOOSE_CMD) down
11+
12+
.PHONY: migrate-status
13+
migrate-status:
14+
$(GOOSE_CMD) status

build.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# create table, init data
2+
goose -dir db/migrations mysql "root:root@tcp(localhost:3306)/db_q_goods_center?parseTime=true" up

config/community_activity.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"category": "community",
3+
"version": "v1",
4+
"name": "社区发帖活动",
5+
"start_at": 1679000000,
6+
"end_at": 1679086400,
7+
"games": [
8+
{
9+
"type": "post",
10+
"name": "发帖奖励",
11+
"config": {
12+
"prize": {
13+
"discount_code": "COMMUNITY_2024",
14+
"price_rule_id": 123,
15+
"probability": 100,
16+
"total_num": 1000,
17+
"remain_num": 1000
18+
},
19+
"state": "OPEN"
20+
}
21+
}
22+
]
23+
}

go.sum

Whitespace-only changes.

models/activity_config.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package models
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
// ActivityConfigJSON 活动配置JSON结构体
9+
type ActivityConfigJSON struct {
10+
Category string `json:"category"` // 活动类型
11+
Version string `json:"version"` // 活动版本
12+
Name string `json:"name"` // 活动名称
13+
StartAt int64 `json:"start_at"` // 开始时间
14+
EndAt int64 `json:"end_at"` // 结束时间
15+
Games []GameConfig `json:"games"` // 玩法配置列表
16+
}
17+
18+
// GameConfig 玩法配置结构体
19+
type GameConfig struct {
20+
Type string `json:"type"` // 玩法类型
21+
Name string `json:"name"` // 玩法名称
22+
Config json.RawMessage `json:"config"` // 玩法具体配置
23+
}
24+
25+
// NewActivityFromConfig 根据配置创建活动实例
26+
func NewActivityFromConfig(configJSON []byte) (ActivityInterface, error) {
27+
var config ActivityConfigJSON
28+
if err := json.Unmarshal(configJSON, &config); err != nil {
29+
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
30+
}
31+
32+
// 根据活动类型创建对应的活动实例
33+
switch config.Category {
34+
case "community":
35+
return NewCommunityActivity(config)
36+
default:
37+
return nil, fmt.Errorf("unsupported activity category: %s", config.Category)
38+
}
39+
}
40+
41+
// NewCommunityActivity 创建社区活动实例
42+
func NewCommunityActivity(config ActivityConfigJSON) (ActivityInterface, error) {
43+
// 解析玩法配置
44+
var games []GameInterface
45+
for _, gameConfig := range config.Games {
46+
game, err := NewGameFromConfig(gameConfig)
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to create game: %w", err)
49+
}
50+
games = append(games, game)
51+
}
52+
53+
return &CommunityActivity{
54+
MetaActivity: MetaActivity{
55+
Category: config.Category,
56+
Version: config.Version,
57+
StartAt: config.StartAt,
58+
EndAt: config.EndAt,
59+
Status: 1, // 默认上线状态
60+
},
61+
GameList: games,
62+
}, nil
63+
}
64+
65+
// NewGameFromConfig 根据配置创建玩法实例
66+
func NewGameFromConfig(config GameConfig) (GameInterface, error) {
67+
switch config.Type {
68+
case "post":
69+
var game CommunityPostGame
70+
if err := json.Unmarshal(config.Config, &game); err != nil {
71+
return nil, fmt.Errorf("failed to unmarshal game config: %w", err)
72+
}
73+
game.Name_ = config.Name
74+
return &game, nil
75+
default:
76+
return nil, fmt.Errorf("unsupported game type: %s", config.Type)
77+
}
78+
}
79+
80+
// CommunityActivity 社区活动实现
81+
type CommunityActivity struct {
82+
MetaActivity
83+
GameList []GameInterface
84+
}
85+
86+
func (a *CommunityActivity) Category() string {
87+
return a.MetaActivity.Category
88+
}
89+
90+
func (a *CommunityActivity) Version() string {
91+
return a.MetaActivity.Version
92+
}
93+
94+
func (a *CommunityActivity) Name() string {
95+
return a.MetaActivity.ActivityConfig.Activity.Name()
96+
}
97+
98+
func (a *CommunityActivity) Games() []GameInterface {
99+
return a.GameList
100+
}

models/community_activity.go

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,131 @@ package models
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
)
78

8-
// 简单活动,只有一个玩法
9-
// 玩法就是发帖,发帖完成之后就可以发奖品了
10-
9+
// CommunityPostGame 社区发帖玩法
1110
type CommunityPostGame struct {
12-
Name string `json:"name"`
13-
Prize PrizeInterface // 以旧换新的奖品
11+
Name_ string `json:"-"` // 玩法名称,从GameConfig中获取
12+
Prize *DiscountCodePrize `json:"prize"`
13+
State GameState `json:"state"`
1414
}
1515

16-
func (p CommunityPostGame) Game(parent string) string {
17-
return parent + p.Name
16+
// Name 返回玩法名称
17+
func (p CommunityPostGame) Name(ctx context.Context) string {
18+
return p.Name_
1819
}
1920

20-
func (p CommunityPostGame) Perform(user User, action ActionInterface) (ResultInterface, error) {
21-
// TODO: select db
22-
fmt.Println(user)
23-
if user.Uid == "" {
24-
return nil, fmt.Errorf("user is nil")
21+
// Perform 执行玩法逻辑
22+
func (p CommunityPostGame) Perform(ctx context.Context, user User, action ActionInterface) (ResultInterface, error) {
23+
// 1. 检查玩法状态
24+
if p.GameState(ctx) != GameStateOPEN {
25+
return nil, fmt.Errorf("game is not open")
26+
}
27+
28+
// 2. 检查用户状态
29+
if p.UserState(ctx) != UserStateOPEN {
30+
return nil, fmt.Errorf("user cannot participate")
2531
}
2632

27-
// 这里判断一下是否需要发奖
28-
// 成功后发奖
33+
// 3. 验证用户是否已发帖
34+
// TODO: 调用社区服务检查用户是否已发帖
35+
// checkUserPost(ctx, user.Uid)
36+
37+
// 4. 发放折扣码奖励
2938
if p.Prize != nil {
30-
err := p.Prize.WinPrize(context.Background(), user)
39+
err := p.Prize.WinPrize(ctx, user)
3140
if err != nil {
32-
return nil, err
41+
return nil, fmt.Errorf("failed to give prize: %w", err)
3342
}
3443
}
3544

36-
return nil, nil
45+
// 5. 记录用户参与状态
46+
// TODO: 更新用户参与记录
47+
// updateUserGameRecord(ctx, user.Uid, UserStateCLOSED)
48+
49+
return &CommunityPostResult{
50+
GameName: p.Name_,
51+
Prize: p.Prize,
52+
}, nil
3753
}
3854

39-
func (p CommunityPostGame) Actions() []ActionInterface {
40-
return []ActionInterface{}
55+
// GameState 返回玩法状态
56+
func (p CommunityPostGame) GameState(ctx context.Context) GameState {
57+
return p.State
4158
}
4259

43-
func (p CommunityPostGame) Results() []ResultInterface {
44-
return []ResultInterface{}
60+
// UserState 返回用户参与状态
61+
func (p CommunityPostGame) UserState(ctx context.Context) UserState {
62+
// TODO:
63+
// 1. 检查用户是否已经参与过
64+
// 2. 检查用户是否已经获得奖励
65+
// 3. 返回对应状态
66+
return UserStateOPEN
4567
}
4668

47-
func (p CommunityPostGame) ValidateConfig() error {
48-
//TODO implement me
49-
if p.Name == "" {
50-
return fmt.Errorf("name is empty")
69+
// ValidateConfig 验证配置
70+
func (p CommunityPostGame) ValidateConfig(ctx context.Context) error {
71+
if p.Prize == nil {
72+
return fmt.Errorf("prize is not configured")
5173
}
5274
return nil
5375
}
5476

55-
func (p CommunityPostGame) UnmarshalJSON(bytes []byte) error {
56-
//TODO implement me
57-
panic("implement me")
77+
// Actions 返回支持的操作
78+
func (p CommunityPostGame) Actions(ctx context.Context) []ActionInterface {
79+
return []ActionInterface{
80+
&CommunityPostAction{}, // 定义发帖动作
81+
}
5882
}
5983

84+
// Results 返回支持的结果
85+
func (p CommunityPostGame) Results(ctx context.Context) []ResultInterface {
86+
return []ResultInterface{
87+
&CommunityPostResult{}, // 定义结果类型
88+
}
89+
}
90+
91+
// MarshalJSON 实现json.Marshaler接口
6092
func (p CommunityPostGame) MarshalJSON() ([]byte, error) {
61-
//TODO implement me
62-
panic("implement me")
93+
type Alias CommunityPostGame
94+
return json.Marshal(&struct {
95+
*Alias
96+
}{
97+
Alias: (*Alias)(&p),
98+
})
99+
}
100+
101+
// UnmarshalJSON 实现json.Unmarshaler接口
102+
func (p *CommunityPostGame) UnmarshalJSON(data []byte) error {
103+
type Alias CommunityPostGame
104+
aux := &struct {
105+
*Alias
106+
}{
107+
Alias: (*Alias)(p),
108+
}
109+
if err := json.Unmarshal(data, &aux); err != nil {
110+
return err
111+
}
112+
return nil
113+
}
114+
115+
// CommunityPostAction 发帖动作
116+
type CommunityPostAction struct {
117+
PostID string `json:"post_id"` // 用户发帖ID
118+
}
119+
120+
func (a CommunityPostAction) Target(ctx context.Context) string {
121+
return "community_post"
122+
}
123+
124+
// CommunityPostResult 发帖结果
125+
type CommunityPostResult struct {
126+
GameName string `json:"game_name"`
127+
Prize *DiscountCodePrize `json:"prize"`
128+
}
129+
130+
func (r CommunityPostResult) Target(ctx context.Context) string {
131+
return r.GameName
63132
}

models/discount_code.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
11
package models
22

3-
import "context"
3+
import (
4+
"context"
5+
"fmt"
6+
)
47

58
// 折扣码的奖品
69

10+
// DiscountCodePrize 折扣码奖品
711
type DiscountCodePrize struct {
8-
DiscountCode string `json:"discount_code"`
9-
PriceRuleID int64 `json:"price_rule_id"`
10-
Probability int64 `json:"probability"` // 中奖概率
12+
DiscountCode string `json:"discount_code"` // 折扣码前缀
13+
PriceRuleID int64 `json:"price_rule_id"` // 价格规则ID
14+
Probability int64 `json:"probability"` // 中奖概率
15+
TotalNum int64 `json:"total_num"` // 总数量
16+
RemainNum int64 `json:"remain_num"` // 剩余数量
1117
}
1218

1319
func (p DiscountCodePrize) WinPrize(ctx context.Context, user User) error {
14-
// 找到一个空的,直接分配给用户J
20+
// 1. 检查库存
21+
if p.RemainNum <= 0 {
22+
return fmt.Errorf("prize stock is empty")
23+
}
1524

16-
//
25+
// 2. 找到一个空的,直接分配给用户
26+
// TODO: 实现折扣码分配逻辑
1727

28+
// 3. 更新库存
29+
p.RemainNum--
1830
return nil
1931
}
2032

2133
func (p DiscountCodePrize) WinProbability() int64 {
22-
//TODO implement me
2334
return p.Probability
2435
}

models/game.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package models
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
)
7+
8+
// GameInterface 是对目前Shopping项目中所有玩法的公共抽象
9+
// Method 中第一个参数为context的出发点是传递上下文,例如tracing等场景
10+
type GameInterface interface {
11+
// Perform 当请求到来时,游戏实例需要执行的业务逻辑
12+
Perform(ctx context.Context, user User, action ActionInterface) (ResultInterface, error)
13+
// ValidateConfig 校验配置是否合法, ctx 用于传递活动和玩法的上下文信息
14+
ValidateConfig(ctx context.Context) error
15+
16+
Name(ctx context.Context) string // 游戏的名称,在同一个活动中,游戏名称必须保证唯一
17+
Actions(ctx context.Context) []ActionInterface // 游戏支持哪些请求
18+
Results(ctx context.Context) []ResultInterface // 游戏支持哪些响应
19+
GameState(ctx context.Context) GameState // 玩法状态
20+
UserState(ctx context.Context) UserState // 用户参与结果
21+
22+
json.Unmarshaler // 非业务功能
23+
json.Marshaler // 非业务功能
24+
}
25+
26+
type GameState = string
27+
28+
const (
29+
GameStateUNKNOWN GameState = "UNKNOWN" // 占位用
30+
GameStateOPEN GameState = "OPEN" // 还可以继续参加
31+
GameStateCLOSED GameState = "CLOSED" // 玩法关闭
32+
)

0 commit comments

Comments
 (0)