| @@ -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") | |||
| } | |||
| @@ -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"` | |||
| @@ -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"` | |||
| @@ -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"` | |||
| } | |||
| @@ -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"` | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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) | |||
| @@ -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 { | |||
| @@ -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()) | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| package operate | |||
| package reward | |||
| type CallbackHandler struct { | |||
| } | |||
| @@ -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] | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| package reward | |||
| type Reward struct { | |||
| Amount int | |||
| Amount int64 | |||
| Type string | |||
| } | |||
| @@ -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] | |||
| } | |||
| @@ -1,10 +0,0 @@ | |||
| package task | |||
| func Accomplish() error { | |||
| //1、幂等性判断 | |||
| //2、获取任务配置 | |||
| //3、判断任务是否可以完成 | |||
| //4、生成任务记录 | |||
| //5、触发奖励发放 | |||
| return nil | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||