diff --git a/models/error.go b/models/error.go index 46917e15e..19afa9d8b 100755 --- a/models/error.go +++ b/models/error.go @@ -2012,3 +2012,15 @@ func IsErrTagNotExist(err error) bool { _, ok := err.(ErrTagNotExist) return ok } + +type ErrRecordNotExist struct { +} + +func IsErrRecordNotExist(err error) bool { + _, ok := err.(ErrRecordNotExist) + return ok +} + +func (err ErrRecordNotExist) Error() string { + return fmt.Sprintf("record not exist in database") +} diff --git a/models/point_account_log.go b/models/point_account_log.go index ae718fe0f..f699495e7 100644 --- a/models/point_account_log.go +++ b/models/point_account_log.go @@ -7,7 +7,7 @@ type PointAccountLog struct { AccountId int64 `xorm:"INDEX NOT NULL"` UserId int64 `xorm:"INDEX NOT NULL"` Type string `xorm:"NOT NULL"` - RelatedId string `xorm:"INDEX NOT NULL"` + SourceId string `xorm:"INDEX NOT NULL"` PointsAmount int64 `xorm:"NOT NULL"` AmountBefore int64 `xorm:"NOT NULL"` AmountAfter int64 `xorm:"NOT NULL"` diff --git a/models/point_operate_record.go b/models/point_operate_record.go index d2dda7863..b0ffb094c 100644 --- a/models/point_operate_record.go +++ b/models/point_operate_record.go @@ -5,9 +5,15 @@ import "code.gitea.io/gitea/modules/timeutil" type RewardSourceType string const ( - SourceTypeAccomplishPointTask RewardSourceType = "ACCOMPLISH_POINT_TASK" - SourceTypeAdminOperate RewardSourceType = "ADMIN_OPERATE" - SourceTypeRunCloudbrainTask RewardSourceType = "RUN_CLOUBRAIN_TASK" + SourceTypeAccomplishTask RewardSourceType = "ACCOMPLISH_TASK" + SourceTypeAdminOperate RewardSourceType = "ADMIN_OPERATE" + SourceTypeRunCloudbrainTask RewardSourceType = "RUN_CLOUBRAIN_TASK" +) + +type RewardType string + +const ( + RewardTypePoint RewardType = "POINT" ) const ( @@ -26,7 +32,7 @@ type PointOperateRecord struct { UserId int64 `xorm:"INDEX NOT NULL"` PointsAmount int64 `xorm:"NOT NULL"` RelatedType string `xorm:"NOT NULL"` - RelatedId string `xorm:"INDEX NOT NULL"` + SourceId string `xorm:"INDEX NOT NULL"` OperateType string `xorm:"NOT NULL"` OperateRate string `xorm:"NOT NULL default once"` Status string `xorm:"NOT NULL"` diff --git a/models/point_task_accomplish_log.go b/models/point_task_accomplish_log.go deleted file mode 100644 index 82c45e163..000000000 --- a/models/point_task_accomplish_log.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import "code.gitea.io/gitea/modules/timeutil" - -type PointTaskAccomplishLog struct { - ID int64 `xorm:"pk autoincr"` - ConfigId int64 `xorm:"NOT NULL"` - TaskCode int64 `xorm:"NOT NULL"` - UserId int64 `xorm:"INDEX NOT NULL"` - RelatedId int64 `xorm:"INDEX NOT NULL"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` -} diff --git a/models/point_task_config.go b/models/point_task_config.go deleted file mode 100644 index 070e3d29e..000000000 --- a/models/point_task_config.go +++ /dev/null @@ -1,25 +0,0 @@ -package models - -import ( - "code.gitea.io/gitea/modules/timeutil" -) - -const ( - TaskConfigRefreshRateOnce = "ONCE" - TaskConfigRefreshRateDaily = "DAILY" -) - -//PointTaskConfig Only add and delete are allowed, edit is not allowed -//so if you want to edit config for some task code,please delete first and add new one -type PointTaskConfig struct { - ID int64 `xorm:"pk autoincr"` - TaskCode string `xorm:"NOT NULL"` - Tittle string `xorm:"NOT NULL"` - RefreshRate string `xorm:"NOT NULL"` - Times int `xorm:"NOT NULL"` - AwardPoints int `xorm:"NOT NULL"` - Status int `xorm:"NOT NULL"` - Creator int64 `xorm:"NOT NULL"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` - DeletedAt timeutil.TimeStamp `xorm:"deleted"` -} diff --git a/models/task_accomplish_log.go b/models/task_accomplish_log.go new file mode 100644 index 000000000..51976c401 --- /dev/null +++ b/models/task_accomplish_log.go @@ -0,0 +1,51 @@ +package models + +import ( + "code.gitea.io/gitea/modules/timeutil" + "time" +) + +type TaskAccomplishLog struct { + ID int64 `xorm:"pk autoincr"` + ConfigId int64 `xorm:"NOT NULL"` + TaskCode string `xorm:"NOT NULL"` + UserId int64 `xorm:"INDEX NOT NULL"` + SourceId string `xorm:"INDEX NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +type LimiterPeriod struct { + StartTime time.Time + EndTime time.Time +} + +func getTaskAccomplishLog(tl *TaskAccomplishLog) (*TaskAccomplishLog, error) { + has, err := x.Get(tl) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRecordNotExist{} + } + return tl, nil +} + +func GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskCode string) (*TaskAccomplishLog, error) { + t := &TaskAccomplishLog{ + SourceId: sourceId, + TaskCode: taskCode, + } + return getTaskAccomplishLog(t) +} + +func CountOnceTask(configId int64, userId int64, period *LimiterPeriod) (int64, error) { + if period == nil { + return x.Where("config_id = ? and user_id = ?", configId, userId).Count(&TaskAccomplishLog{}) + } else { + return x.Where("config_id = ? and user_id = ? and created_unix >= ? and created_unix < ? ", configId, userId, period.StartTime.Unix(), period.EndTime.Unix()).Count(&TaskAccomplishLog{}) + } + +} + +func InsertTaskAccomplishLog(tl *TaskAccomplishLog) (int64, error) { + return x.Insert(tl) +} diff --git a/models/task_config.go b/models/task_config.go new file mode 100644 index 000000000..f74237b59 --- /dev/null +++ b/models/task_config.go @@ -0,0 +1,53 @@ +package models + +import ( + "code.gitea.io/gitea/modules/timeutil" + "fmt" +) + +type TaskType string + +const ( + TaskTypeComment TaskType = "COMMENT" +) + +func (t *TaskType) String() string { + return fmt.Sprint(t) +} + +const ( + TaskConfigRefreshRateNotCycle = "NOT_CYCLE" + TaskConfigRefreshRateDaily = "DAILY" +) + +//PointTaskConfig Only add and delete are allowed, edit is not allowed +//so if you want to edit config for some task code,please delete first and add new one +type TaskConfig struct { + ID int64 `xorm:"pk autoincr"` + TaskCode string `xorm:"NOT NULL"` + Tittle string `xorm:"NOT NULL"` + RefreshRate string `xorm:"NOT NULL"` + Times int64 `xorm:"NOT NULL"` + AwardType string `xorm:"NOT NULL"` + AwardAmount int64 `xorm:"NOT NULL"` + Creator int64 `xorm:"NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + DeletedAt timeutil.TimeStamp `xorm:"deleted"` +} + +func getTaskConfig(t *TaskConfig) (*TaskConfig, error) { + has, err := x.Get(t) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRecordNotExist{} + } + return t, nil +} + +func GetTaskConfigByTaskCode(taskCode string) (*TaskConfig, error) { + t := &TaskConfig{ + TaskCode: taskCode, + } + return getTaskConfig(t) +} diff --git a/modules/auth/wechat/access_token.go b/modules/auth/wechat/access_token.go index f9516e3e1..af62c3e7b 100644 --- a/modules/auth/wechat/access_token.go +++ b/modules/auth/wechat/access_token.go @@ -7,14 +7,12 @@ import ( "time" ) -const EMPTY_REDIS_VAL = "Nil" - var accessTokenLock = redis_lock.NewDistributeLock(redis_key.AccessTokenLockKey()) func GetWechatAccessToken() string { token, _ := redis_client.Get(redis_key.WechatAccessTokenKey()) if token != "" { - if token == EMPTY_REDIS_VAL { + if token == redis_key.EMPTY_REDIS_VAL { return "" } live, _ := redis_client.TTL(redis_key.WechatAccessTokenKey()) @@ -39,7 +37,7 @@ func refreshAndGetAccessToken() string { defer accessTokenLock.UnLock() token, _ := redis_client.Get(redis_key.WechatAccessTokenKey()) if token != "" { - if token == EMPTY_REDIS_VAL { + if token == redis_key.EMPTY_REDIS_VAL { return "" } return token @@ -59,7 +57,7 @@ func callAccessTokenAndUpdateCache() string { } if token == "" { - redis_client.Setex(redis_key.WechatAccessTokenKey(), EMPTY_REDIS_VAL, 10*time.Second) + redis_client.Setex(redis_key.WechatAccessTokenKey(), redis_key.EMPTY_REDIS_VAL, 10*time.Second) return "" } redis_client.Setex(redis_key.WechatAccessTokenKey(), token, time.Duration(r.Expires_in)*time.Second) diff --git a/modules/redis/redis_key/key_base.go b/modules/redis/redis_key/key_base.go index 0efc6ed38..797720c62 100644 --- a/modules/redis/redis_key/key_base.go +++ b/modules/redis/redis_key/key_base.go @@ -4,6 +4,8 @@ import "strings" const KEY_SEPARATE = ":" +const EMPTY_REDIS_VAL = "Nil" + func KeyJoin(keys ...string) string { var build strings.Builder for _, v := range keys { diff --git a/modules/redis/redis_key/task_redis_key.go b/modules/redis/redis_key/task_redis_key.go new file mode 100644 index 000000000..b33e575fb --- /dev/null +++ b/modules/redis/redis_key/task_redis_key.go @@ -0,0 +1,16 @@ +package redis_key + +import ( + "code.gitea.io/gitea/models" + "fmt" +) + +const TASK_REDIS_PREFIX = "task" + +func TaskAccomplishLock(userId int64, sourceId string, taskType models.TaskType) string { + return KeyJoin(TASK_REDIS_PREFIX, fmt.Sprint(userId), sourceId, taskType.String(), "accomplish") +} + +func TaskConfig(taskType models.TaskType) string { + return KeyJoin(TASK_REDIS_PREFIX, "config", taskType.String()) +} diff --git a/services/reward/operate/callback.go b/services/reward/callback.go similarity index 67% rename from services/reward/operate/callback.go rename to services/reward/callback.go index 27c42f443..b67ffa673 100644 --- a/services/reward/operate/callback.go +++ b/services/reward/callback.go @@ -1,4 +1,4 @@ -package operate +package reward type CallbackHandler struct { } diff --git a/services/reward/operate/operator.go b/services/reward/operator.go similarity index 58% rename from services/reward/operate/operator.go rename to services/reward/operator.go index 63d12b970..848ba703d 100644 --- a/services/reward/operate/operator.go +++ b/services/reward/operator.go @@ -1,17 +1,22 @@ -package operate +package reward import ( "code.gitea.io/gitea/models" - "code.gitea.io/gitea/services/reward" + "code.gitea.io/gitea/services/reward/point" + "errors" + "fmt" ) +var RewardOperatorMap = map[string]RewardOperator{ + fmt.Sprint(models.RewardTypePoint): new(point.PointOperator), +} + type RewardOperateContext struct { SourceType models.RewardSourceType - RelatedId string + SourceId string Remark string - Reward reward.Reward + Reward Reward TargetUserId int64 - RequestId string } type RewardOperateResponse int @@ -31,7 +36,11 @@ type RewardOperator interface { Operate(ctx RewardOperateContext) error } -func Operate(operator RewardOperator, ctx RewardOperateContext) error { +func Send(ctx RewardOperateContext) error { + operator := GetOperator(ctx.Reward.Type) + if operator == nil { + return errors.New("operator of reward type is not exist") + } if operator.IsOperated(ctx) { return nil } @@ -43,3 +52,7 @@ func Operate(operator RewardOperator, ctx RewardOperateContext) error { } return nil } + +func GetOperator(rewardType string) RewardOperator { + return RewardOperatorMap[rewardType] +} diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index f91ca11b3..5a6c18bff 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -1,16 +1,19 @@ package point -import "code.gitea.io/gitea/services/reward/operate" +import ( + "code.gitea.io/gitea/services/reward" +) type PointOperator struct { } -func (operator *PointOperator) IsOperated(ctx operate.RewardOperateContext) bool { +func (operator *PointOperator) IsOperated(ctx reward.RewardOperateContext) bool { + //todo return true } -func (operator *PointOperator) IsLimited(ctx operate.RewardOperateContext) bool { +func (operator *PointOperator) IsLimited(ctx reward.RewardOperateContext) bool { return true } -func (operator *PointOperator) Operate(ctx operate.RewardOperateContext) error { +func (operator *PointOperator) Operate(ctx reward.RewardOperateContext) error { return nil } diff --git a/services/reward/reward.go b/services/reward/reward.go index 8ec0e0471..ca1c1f3cd 100644 --- a/services/reward/reward.go +++ b/services/reward/reward.go @@ -1,6 +1,6 @@ package reward type Reward struct { - Amount int + Amount int64 Type string } diff --git a/services/task/limiter.go b/services/task/limiter.go new file mode 100644 index 000000000..6c2cd4f44 --- /dev/null +++ b/services/task/limiter.go @@ -0,0 +1,39 @@ +package task + +import ( + "code.gitea.io/gitea/models" + "time" +) + +var LimiterMap = map[string]Limiter{ + models.TaskConfigRefreshRateNotCycle: new(NoCycleLimiter), + models.TaskConfigRefreshRateDaily: new(DailyLimiter), +} + +type Limiter interface { + GetCurrentPeriod() *models.LimiterPeriod +} + +type NoCycleLimiter struct { +} + +func (l *NoCycleLimiter) GetCurrentPeriod() *models.LimiterPeriod { + return nil +} + +type DailyLimiter struct { +} + +func (l *DailyLimiter) GetCurrentPeriod() *models.LimiterPeriod { + t := time.Now() + startTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + endTime := startTime.Add(24 * time.Hour) + return &models.LimiterPeriod{ + StartTime: startTime, + EndTime: endTime, + } +} + +func GetLimiter(refreshRateype string) Limiter { + return LimiterMap[refreshRateype] +} diff --git a/services/task/point_task.go b/services/task/point_task.go deleted file mode 100644 index b72fbffdc..000000000 --- a/services/task/point_task.go +++ /dev/null @@ -1,10 +0,0 @@ -package task - -func Accomplish() error { - //1、幂等性判断 - //2、获取任务配置 - //3、判断任务是否可以完成 - //4、生成任务记录 - //5、触发奖励发放 - return nil -} diff --git a/services/task/task.go b/services/task/task.go new file mode 100644 index 000000000..3b702f179 --- /dev/null +++ b/services/task/task.go @@ -0,0 +1,104 @@ +package task + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/redis/redis_lock" + "code.gitea.io/gitea/services/reward" + "errors" + "time" +) + +func Accomplish(userId int64, taskType models.TaskType, sourceId string) { + go accomplish(userId, taskType, sourceId) +} + +func accomplish(userId int64, taskType models.TaskType, sourceId string) error { + //lock + var taskLock = redis_lock.NewDistributeLock(redis_key.TaskAccomplishLock(userId, sourceId, taskType)) + if !taskLock.Lock(3 * time.Second) { + log.Info("duplicated task request,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) + return nil + } + defer taskLock.UnLock() + + //is handled before? + isHandled, err := isHandled(taskType, sourceId) + if err != nil { + log.Error("Get isHandled error,%v", err) + return err + } + if isHandled { + log.Info("task has been handled,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) + return nil + } + + //get task config + config, err := GetTaskConfig(taskType) + if err != nil { + log.Error("GetTaskConfig error,%v", err) + return err + } + if config == nil { + log.Info("task config not exist,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) + return nil + } + + //is limited? + isLimited, err := IsLimited(userId, config) + if err != nil { + log.Error("get limited error,%v", err) + return err + } + if isLimited { + log.Info("task accomplish maximum times are reached,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) + return nil + } + + //add log + models.InsertTaskAccomplishLog(&models.TaskAccomplishLog{ + ConfigId: config.ID, + TaskCode: config.TaskCode, + UserId: userId, + SourceId: sourceId, + }) + + //reward + reward.Send(reward.RewardOperateContext{ + SourceType: models.SourceTypeAccomplishTask, + SourceId: sourceId, + Reward: reward.Reward{ + Amount: config.AwardAmount, + Type: config.AwardType, + }, + TargetUserId: userId, + }) + + return nil +} + +func isHandled(taskType models.TaskType, sourceId string) (bool, error) { + _, err := models.GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskType.String()) + if err != nil { + if models.IsErrRecordNotExist(err) { + return false, nil + } + return false, err + } + return true, nil + +} + +func IsLimited(userId int64, config *models.TaskConfig) (bool, error) { + limiter := GetLimiter(config.RefreshRate) + if limiter == nil { + return false, errors.New("task config incorrect") + } + n, err := models.CountOnceTask(config.ID, userId, limiter.GetCurrentPeriod()) + if err != nil { + return false, err + } + return n >= config.Times, nil + +} diff --git a/services/task/task_config.go b/services/task/task_config.go new file mode 100644 index 000000000..ccdf4c08a --- /dev/null +++ b/services/task/task_config.go @@ -0,0 +1,28 @@ +package task + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" + "encoding/json" +) + +func GetTaskConfig(taskType models.TaskType) (*models.TaskConfig, error) { + configStr, _ := redis_client.Get(redis_key.TaskConfig(taskType)) + if configStr != "" { + if configStr == redis_key.EMPTY_REDIS_VAL { + return nil, nil + } + config := new(models.TaskConfig) + json.Unmarshal([]byte(configStr), config) + return config, nil + } + config, err := models.GetTaskConfigByTaskCode(taskType.String()) + if err != nil { + if models.IsErrRecordNotExist(err) { + return nil, nil + } + return nil, err + } + return config, nil +}