From e553d022338d12868d27694036e5628c742cdbb0 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 31 May 2022 11:55:10 +0800 Subject: [PATCH 001/283] #1249 add models --- models/point_account.go | 32 ++++++++++++++++++ models/point_account_log.go | 16 +++++++++ models/point_limit_config.go | 23 +++++++++++++ models/point_operate_record.go | 36 +++++++++++++++++++++ models/point_periodic_task.go | 28 ++++++++++++++++ models/point_task_accomplish_log.go | 12 +++++++ models/point_task_config.go | 25 ++++++++++++++ services/reward/account/account.go | 9 ++++++ services/reward/operate/callback.go | 4 +++ services/reward/operate/operator.go | 45 ++++++++++++++++++++++++++ services/reward/point/point_operate.go | 16 +++++++++ services/reward/reward.go | 6 ++++ services/task/point_task.go | 10 ++++++ 13 files changed, 262 insertions(+) create mode 100644 models/point_account.go create mode 100644 models/point_account_log.go create mode 100644 models/point_limit_config.go create mode 100644 models/point_operate_record.go create mode 100644 models/point_periodic_task.go create mode 100644 models/point_task_accomplish_log.go create mode 100644 models/point_task_config.go create mode 100644 services/reward/account/account.go create mode 100644 services/reward/operate/callback.go create mode 100644 services/reward/operate/operator.go create mode 100644 services/reward/point/point_operate.go create mode 100644 services/reward/reward.go create mode 100644 services/task/point_task.go diff --git a/models/point_account.go b/models/point_account.go new file mode 100644 index 000000000..f889d5d4f --- /dev/null +++ b/models/point_account.go @@ -0,0 +1,32 @@ +package models + +import "code.gitea.io/gitea/modules/timeutil" + +type PointAccountStatus int + +// Possible PointAccountStatus types. +const ( + PointAccountNormal PointAccountStatus = iota + 1 // 1 + PointAccountFreeze // 2 + PointAccountDeleted // 3 +) + +type PointAccount struct { + ID int64 `xorm:"pk autoincr"` + Balance int64 `xorm:"NOT NULL DEFAULT 0"` + TotalEarned int64 `xorm:"NOT NULL DEFAULT 0"` + TotalConsumed int64 `xorm:"NOT NULL DEFAULT 0"` + UserId int64 `xorm:"INDEX NOT NULL"` + Status string `xorm:"NOT NULL"` + Version int64 `xorm:"NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +func (account *PointAccount) Increase(amount int64) error { + return nil +} + +func (account *PointAccount) Decrease(amount int64) error { + return nil +} diff --git a/models/point_account_log.go b/models/point_account_log.go new file mode 100644 index 000000000..ae718fe0f --- /dev/null +++ b/models/point_account_log.go @@ -0,0 +1,16 @@ +package models + +import "code.gitea.io/gitea/modules/timeutil" + +type PointAccountLog struct { + ID int64 `xorm:"pk autoincr"` + AccountId int64 `xorm:"INDEX NOT NULL"` + UserId int64 `xorm:"INDEX NOT NULL"` + Type string `xorm:"NOT NULL"` + RelatedId string `xorm:"INDEX NOT NULL"` + PointsAmount int64 `xorm:"NOT NULL"` + AmountBefore int64 `xorm:"NOT NULL"` + AmountAfter int64 `xorm:"NOT NULL"` + AccountVersion int64 `xorm:"NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} diff --git a/models/point_limit_config.go b/models/point_limit_config.go new file mode 100644 index 000000000..60fb5e735 --- /dev/null +++ b/models/point_limit_config.go @@ -0,0 +1,23 @@ +package models + +import "code.gitea.io/gitea/modules/timeutil" + +const ( + LimitConfigRefreshRateOnce = "ONCE" + LimitConfigRefreshRateDaily = "DAILY" +) +const ( + LimitTargetRangeAllUser = "ALL_USER" + LimitTargetRangeSingleUser = "SINGLE_USER" +) + +type PointLimitConfig struct { + ID int64 `xorm:"pk autoincr"` + Tittle string + RefreshRate string `xorm:"NOT NULL"` + TargetRange string `xorm:"NOT NULL"` + LimitNum int64 `xorm:"NOT NULL"` + Creator int64 `xorm:"NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + DeletedAt timeutil.TimeStamp `xorm:"deleted"` +} diff --git a/models/point_operate_record.go b/models/point_operate_record.go new file mode 100644 index 000000000..d2dda7863 --- /dev/null +++ b/models/point_operate_record.go @@ -0,0 +1,36 @@ +package models + +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" +) + +const ( + OperateTypeIncrease = "INCREASE_POINT" + OperateTypeDecrease = "DECREASE_POINT" +) + +const ( + OperateStatusOperating = "OPERATING" + OperateStatusSucceeded = "SUCCEEDED" + OperateStatusFailed = "FAILED" +) + +type PointOperateRecord struct { + ID int64 `xorm:"pk autoincr"` + UserId int64 `xorm:"INDEX NOT NULL"` + PointsAmount int64 `xorm:"NOT NULL"` + RelatedType string `xorm:"NOT NULL"` + RelatedId string `xorm:"INDEX NOT NULL"` + OperateType string `xorm:"NOT NULL"` + OperateRate string `xorm:"NOT NULL default once"` + Status string `xorm:"NOT NULL"` + Remark string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} diff --git a/models/point_periodic_task.go b/models/point_periodic_task.go new file mode 100644 index 000000000..0d4297f2f --- /dev/null +++ b/models/point_periodic_task.go @@ -0,0 +1,28 @@ +package models + +import "code.gitea.io/gitea/modules/timeutil" + +type PeriodicTaskStatus int + +// Possible PeriodicTaskStatus types. +const ( + PeriodicTaskStatusRunning PointAccountStatus = iota + 1 // 1 + PeriodicTaskStatusSuccess // 2 + PeriodicTaskStatusFailed // 3 +) + +type PeriodicTask struct { + ID int64 `xorm:"pk autoincr"` + Type string `xorm:"NOT NULL"` + OperateRecordId int64 `xorm:"INDEX NOT NULL"` + IntervalSecond int64 `xorm:"NOT NULL"` + PointsAmount int64 `xorm:"NOT NULL"` + NextExecuteTime timeutil.TimeStamp + SuccessCount int `xorm:"NOT NULL default 0"` + FailedCount int `xorm:"NOT NULL default 0"` + Status string `xorm:"NOT NULL"` + ExitCode string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + FinishedUnix timeutil.TimeStamp `xorm:"INDEX"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} diff --git a/models/point_task_accomplish_log.go b/models/point_task_accomplish_log.go new file mode 100644 index 000000000..82c45e163 --- /dev/null +++ b/models/point_task_accomplish_log.go @@ -0,0 +1,12 @@ +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 new file mode 100644 index 000000000..070e3d29e --- /dev/null +++ b/models/point_task_config.go @@ -0,0 +1,25 @@ +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/services/reward/account/account.go b/services/reward/account/account.go new file mode 100644 index 000000000..4967b368e --- /dev/null +++ b/services/reward/account/account.go @@ -0,0 +1,9 @@ +package account + +func IncreaseAmount(userId int64, amount int64) error { + return nil +} + +func DecreaseAmount(userId int64, amount int64) error { + return nil +} diff --git a/services/reward/operate/callback.go b/services/reward/operate/callback.go new file mode 100644 index 000000000..27c42f443 --- /dev/null +++ b/services/reward/operate/callback.go @@ -0,0 +1,4 @@ +package operate + +type CallbackHandler struct { +} diff --git a/services/reward/operate/operator.go b/services/reward/operate/operator.go new file mode 100644 index 000000000..63d12b970 --- /dev/null +++ b/services/reward/operate/operator.go @@ -0,0 +1,45 @@ +package operate + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/services/reward" +) + +type RewardOperateContext struct { + SourceType models.RewardSourceType + RelatedId string + Remark string + Reward reward.Reward + TargetUserId int64 + RequestId string +} + +type RewardOperateResponse int + +const ( + RewardOperateSuccess RewardOperateResponse = iota + 1 + RewardOperateBalanceNotEnough +) + +func (t RewardOperateResponse) IsSuccess() bool { + return t == RewardOperateSuccess +} + +type RewardOperator interface { + IsOperated(ctx RewardOperateContext) bool + IsLimited(ctx RewardOperateContext) bool + Operate(ctx RewardOperateContext) error +} + +func Operate(operator RewardOperator, ctx RewardOperateContext) error { + if operator.IsOperated(ctx) { + return nil + } + if operator.IsLimited(ctx) { + return nil + } + if err := operator.Operate(ctx); err != nil { + return err + } + return nil +} diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go new file mode 100644 index 000000000..f91ca11b3 --- /dev/null +++ b/services/reward/point/point_operate.go @@ -0,0 +1,16 @@ +package point + +import "code.gitea.io/gitea/services/reward/operate" + +type PointOperator struct { +} + +func (operator *PointOperator) IsOperated(ctx operate.RewardOperateContext) bool { + return true +} +func (operator *PointOperator) IsLimited(ctx operate.RewardOperateContext) bool { + return true +} +func (operator *PointOperator) Operate(ctx operate.RewardOperateContext) error { + return nil +} diff --git a/services/reward/reward.go b/services/reward/reward.go new file mode 100644 index 000000000..8ec0e0471 --- /dev/null +++ b/services/reward/reward.go @@ -0,0 +1,6 @@ +package reward + +type Reward struct { + Amount int + Type string +} diff --git a/services/task/point_task.go b/services/task/point_task.go new file mode 100644 index 000000000..b72fbffdc --- /dev/null +++ b/services/task/point_task.go @@ -0,0 +1,10 @@ +package task + +func Accomplish() error { + //1、幂等性判断 + //2、获取任务配置 + //3、判断任务是否可以完成 + //4、生成任务记录 + //5、触发奖励发放 + return nil +} From 57a590de791a2742130591b3c7fc4693f4d9f1b0 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 31 May 2022 17:43:46 +0800 Subject: [PATCH 002/283] update task and reward --- models/error.go | 12 +++ models/point_account_log.go | 2 +- models/point_operate_record.go | 14 ++- models/point_task_accomplish_log.go | 12 --- models/point_task_config.go | 25 ------ models/task_accomplish_log.go | 51 +++++++++++ models/task_config.go | 53 +++++++++++ modules/auth/wechat/access_token.go | 8 +- modules/redis/redis_key/key_base.go | 2 + modules/redis/redis_key/task_redis_key.go | 16 ++++ services/reward/{operate => }/callback.go | 2 +- services/reward/{operate => }/operator.go | 25 ++++-- services/reward/point/point_operate.go | 11 ++- services/reward/reward.go | 2 +- services/task/limiter.go | 39 ++++++++ services/task/point_task.go | 10 --- services/task/task.go | 104 ++++++++++++++++++++++ services/task/task_config.go | 28 ++++++ 18 files changed, 347 insertions(+), 69 deletions(-) delete mode 100644 models/point_task_accomplish_log.go delete mode 100644 models/point_task_config.go create mode 100644 models/task_accomplish_log.go create mode 100644 models/task_config.go create mode 100644 modules/redis/redis_key/task_redis_key.go rename services/reward/{operate => }/callback.go (67%) rename services/reward/{operate => }/operator.go (58%) create mode 100644 services/task/limiter.go delete mode 100644 services/task/point_task.go create mode 100644 services/task/task.go create mode 100644 services/task/task_config.go 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 +} From 1a2327fc2d86220414a2cecdc6c1fe2a8392e6f9 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 31 May 2022 17:56:31 +0800 Subject: [PATCH 003/283] update --- models/task_accomplish_log.go | 2 +- services/task/task.go | 2 +- services/task/task_config.go | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/models/task_accomplish_log.go b/models/task_accomplish_log.go index 51976c401..ed2927678 100644 --- a/models/task_accomplish_log.go +++ b/models/task_accomplish_log.go @@ -37,7 +37,7 @@ func GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskCode string) (*Task return getTaskAccomplishLog(t) } -func CountOnceTask(configId int64, userId int64, period *LimiterPeriod) (int64, error) { +func CountInTaskPeriod(configId int64, userId int64, period *LimiterPeriod) (int64, error) { if period == nil { return x.Where("config_id = ? and user_id = ?", configId, userId).Count(&TaskAccomplishLog{}) } else { diff --git a/services/task/task.go b/services/task/task.go index 3b702f179..c5f65240b 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -95,7 +95,7 @@ func IsLimited(userId int64, config *models.TaskConfig) (bool, error) { if limiter == nil { return false, errors.New("task config incorrect") } - n, err := models.CountOnceTask(config.ID, userId, limiter.GetCurrentPeriod()) + n, err := models.CountInTaskPeriod(config.ID, userId, limiter.GetCurrentPeriod()) if err != nil { return false, err } diff --git a/services/task/task_config.go b/services/task/task_config.go index ccdf4c08a..6812f5d67 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -5,10 +5,14 @@ import ( "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "encoding/json" + "time" ) +//GetTaskConfig get task config from redis cache first +// if not exist in redis, find in db and refresh the redis key func GetTaskConfig(taskType models.TaskType) (*models.TaskConfig, error) { - configStr, _ := redis_client.Get(redis_key.TaskConfig(taskType)) + redisKey := redis_key.TaskConfig(taskType) + configStr, _ := redis_client.Get(redisKey) if configStr != "" { if configStr == redis_key.EMPTY_REDIS_VAL { return nil, nil @@ -22,7 +26,10 @@ func GetTaskConfig(taskType models.TaskType) (*models.TaskConfig, error) { if models.IsErrRecordNotExist(err) { return nil, nil } + redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) return nil, err } + jsonStr, _ := json.Marshal(config) + redis_client.Setex(redisKey, string(jsonStr), 30*24*time.Hour) return config, nil } From 7f64382856e26cd7db4323297f0c836ce94f6b4a Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 1 Jun 2022 18:09:27 +0800 Subject: [PATCH 004/283] add reward --- models/limit_config.go | 33 ++++++ models/point_account.go | 77 ++++++++++++- models/point_account_log.go | 11 +- models/point_limit_config.go | 23 ---- models/point_operate_record.go | 42 ------- models/reward_operate_record.go | 84 ++++++++++++++ models/task_accomplish_log.go | 6 +- models/task_config.go | 4 +- modules/redis/redis_client/client.go | 45 ++++++++ modules/redis/redis_key/account_redis_key.go | 17 +++ modules/redis/redis_key/limit_redis_key.go | 26 +++++ modules/redis/redis_key/reward_redis_key.go | 7 ++ modules/redis/redis_key/task_redis_key.go | 5 +- modules/util/uuid_util.go | 10 ++ services/reward/account/account.go | 9 -- services/reward/callback.go | 4 - services/reward/limiter/limiter.go | 93 +++++++++++++++ services/reward/operator.go | 106 ++++++++++++++---- .../reward/point/account/point_account.go | 58 ++++++++++ services/reward/point/point_operate.go | 33 +++++- services/task/limiter.go | 39 ------- services/task/period/handler.go | 50 +++++++++ services/task/task.go | 21 ++-- services/task/task_config.go | 2 +- 24 files changed, 640 insertions(+), 165 deletions(-) create mode 100644 models/limit_config.go delete mode 100644 models/point_limit_config.go delete mode 100644 models/point_operate_record.go create mode 100644 models/reward_operate_record.go create mode 100644 modules/redis/redis_key/account_redis_key.go create mode 100644 modules/redis/redis_key/limit_redis_key.go create mode 100644 modules/redis/redis_key/reward_redis_key.go create mode 100644 modules/util/uuid_util.go delete mode 100644 services/reward/account/account.go delete mode 100644 services/reward/callback.go create mode 100644 services/reward/limiter/limiter.go create mode 100644 services/reward/point/account/point_account.go delete mode 100644 services/task/limiter.go create mode 100644 services/task/period/handler.go diff --git a/models/limit_config.go b/models/limit_config.go new file mode 100644 index 000000000..273af0de1 --- /dev/null +++ b/models/limit_config.go @@ -0,0 +1,33 @@ +package models + +import "code.gitea.io/gitea/modules/timeutil" + +type LimitConfig struct { + ID int64 `xorm:"pk autoincr"` + Tittle string + RefreshRate string `xorm:"NOT NULL"` + Scope string `xorm:"NOT NULL"` + LimitNum int64 `xorm:"NOT NULL"` + LimitCode string `xorm:"NOT NULL"` + Creator int64 `xorm:"NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + DeletedAt timeutil.TimeStamp `xorm:"deleted"` +} + +func findLimitConfig(tl *LimitConfig) ([]LimitConfig, error) { + r := make([]LimitConfig, 0) + err := x.Find(r, tl) + if err != nil { + return nil, err + } else if len(r) == 0 { + return nil, ErrRecordNotExist{} + } + return r, nil +} + +func GetLimitConfigByLimitCode(limitCode string) ([]LimitConfig, error) { + t := &LimitConfig{ + LimitCode: limitCode, + } + return findLimitConfig(t) +} diff --git a/models/point_account.go b/models/point_account.go index f889d5d4f..7fa38cb7a 100644 --- a/models/point_account.go +++ b/models/point_account.go @@ -6,27 +6,92 @@ type PointAccountStatus int // Possible PointAccountStatus types. const ( - PointAccountNormal PointAccountStatus = iota + 1 // 1 - PointAccountFreeze // 2 - PointAccountDeleted // 3 + PointAccountNormal int = iota + 1 // 1 + PointAccountFreeze // 2 + PointAccountDeleted // 3 ) type PointAccount struct { ID int64 `xorm:"pk autoincr"` + AccountCode string `xorm:"INDEX NOT NULL"` Balance int64 `xorm:"NOT NULL DEFAULT 0"` TotalEarned int64 `xorm:"NOT NULL DEFAULT 0"` TotalConsumed int64 `xorm:"NOT NULL DEFAULT 0"` UserId int64 `xorm:"INDEX NOT NULL"` - Status string `xorm:"NOT NULL"` + Status int `xorm:"NOT NULL"` Version int64 `xorm:"NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } -func (account *PointAccount) Increase(amount int64) error { +func (account *PointAccount) Increase(amount int64, sourceId string) error { + sess := x.NewSession() + defer sess.Close() + sql := "update point_account set balance = balance + ?,total_earned = total_earned + ? ,version = version + 1 where account_code = ? " + _, err := sess.Exec(sql, amount, amount, account.AccountCode) + if err != nil { + sess.Rollback() + return err + } + accountLog := &PointAccountLog{ + AccountCode: account.AccountCode, + UserId: account.UserId, + Type: IncreaseAccountBalance, + SourceId: sourceId, + PointsAmount: amount, + BalanceBefore: account.Balance, + BalanceAfter: account.Balance + amount, + AccountVersion: account.Version, + } + _, err = sess.Insert(accountLog) + if err != nil { + sess.Rollback() + return err + } + sess.Commit() return nil } -func (account *PointAccount) Decrease(amount int64) error { +func (account *PointAccount) Decrease(amount int64, sourceId string) error { + sess := x.NewSession() + defer sess.Close() + sql := "update point_account set balance = balance - ?,total_consumed = total_consumed + ? ,version = version + 1 where account_code = ? " + _, err := sess.Exec(sql, amount, amount, account.AccountCode) + if err != nil { + sess.Rollback() + return err + } + accountLog := &PointAccountLog{ + AccountCode: account.AccountCode, + UserId: account.UserId, + Type: DecreaseAccountBalance, + SourceId: sourceId, + PointsAmount: amount, + BalanceBefore: account.Balance, + BalanceAfter: account.Balance - amount, + AccountVersion: account.Version, + } + _, err = sess.Insert(accountLog) + if err != nil { + sess.Rollback() + return err + } + sess.Commit() return nil } + +func GetAccountByUserId(userId int64) (*PointAccount, error) { + p := &PointAccount{} + has, err := x.Where("user_id = ?", userId).Get(p) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return p, nil +} + +func InsertAccount(tl *PointAccount) (int64, error) { + return x.Insert(tl) +} diff --git a/models/point_account_log.go b/models/point_account_log.go index f699495e7..3ed39ed77 100644 --- a/models/point_account_log.go +++ b/models/point_account_log.go @@ -2,15 +2,20 @@ package models import "code.gitea.io/gitea/modules/timeutil" +const ( + IncreaseAccountBalance = "increase" + DecreaseAccountBalance = "decrease" +) + type PointAccountLog struct { ID int64 `xorm:"pk autoincr"` - AccountId int64 `xorm:"INDEX NOT NULL"` + AccountCode string `xorm:"INDEX NOT NULL"` UserId int64 `xorm:"INDEX NOT NULL"` Type string `xorm:"NOT NULL"` SourceId string `xorm:"INDEX NOT NULL"` PointsAmount int64 `xorm:"NOT NULL"` - AmountBefore int64 `xorm:"NOT NULL"` - AmountAfter int64 `xorm:"NOT NULL"` + BalanceBefore int64 `xorm:"NOT NULL"` + BalanceAfter int64 `xorm:"NOT NULL"` AccountVersion int64 `xorm:"NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` } diff --git a/models/point_limit_config.go b/models/point_limit_config.go deleted file mode 100644 index 60fb5e735..000000000 --- a/models/point_limit_config.go +++ /dev/null @@ -1,23 +0,0 @@ -package models - -import "code.gitea.io/gitea/modules/timeutil" - -const ( - LimitConfigRefreshRateOnce = "ONCE" - LimitConfigRefreshRateDaily = "DAILY" -) -const ( - LimitTargetRangeAllUser = "ALL_USER" - LimitTargetRangeSingleUser = "SINGLE_USER" -) - -type PointLimitConfig struct { - ID int64 `xorm:"pk autoincr"` - Tittle string - RefreshRate string `xorm:"NOT NULL"` - TargetRange string `xorm:"NOT NULL"` - LimitNum int64 `xorm:"NOT NULL"` - Creator int64 `xorm:"NOT NULL"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` - DeletedAt timeutil.TimeStamp `xorm:"deleted"` -} diff --git a/models/point_operate_record.go b/models/point_operate_record.go deleted file mode 100644 index b0ffb094c..000000000 --- a/models/point_operate_record.go +++ /dev/null @@ -1,42 +0,0 @@ -package models - -import "code.gitea.io/gitea/modules/timeutil" - -type RewardSourceType string - -const ( - SourceTypeAccomplishTask RewardSourceType = "ACCOMPLISH_TASK" - SourceTypeAdminOperate RewardSourceType = "ADMIN_OPERATE" - SourceTypeRunCloudbrainTask RewardSourceType = "RUN_CLOUBRAIN_TASK" -) - -type RewardType string - -const ( - RewardTypePoint RewardType = "POINT" -) - -const ( - OperateTypeIncrease = "INCREASE_POINT" - OperateTypeDecrease = "DECREASE_POINT" -) - -const ( - OperateStatusOperating = "OPERATING" - OperateStatusSucceeded = "SUCCEEDED" - OperateStatusFailed = "FAILED" -) - -type PointOperateRecord struct { - ID int64 `xorm:"pk autoincr"` - UserId int64 `xorm:"INDEX NOT NULL"` - PointsAmount int64 `xorm:"NOT NULL"` - RelatedType string `xorm:"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"` - Remark string - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` -} diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go new file mode 100644 index 000000000..ca1f52168 --- /dev/null +++ b/models/reward_operate_record.go @@ -0,0 +1,84 @@ +package models + +import ( + "code.gitea.io/gitea/modules/timeutil" + "fmt" +) + +type RewardSourceType string + +const ( + SourceTypeAccomplishTask RewardSourceType = "ACCOMPLISH_TASK" + SourceTypeAdminOperate RewardSourceType = "ADMIN_OPERATE" + SourceTypeRunCloudbrainTask RewardSourceType = "RUN_CLOUBRAIN_TASK" +) + +func (r *RewardSourceType) String() string { + return fmt.Sprint(r) +} + +type RewardType string + +const ( + RewardTypePoint RewardType = "POINT" +) + +func (r *RewardType) String() string { + return fmt.Sprint(r) +} + +const ( + OperateTypeIncrease = "INCREASE_POINT" + OperateTypeDecrease = "DECREASE_POINT" +) + +const ( + OperateStatusOperating = "OPERATING" + OperateStatusSucceeded = "SUCCEEDED" + OperateStatusFailed = "FAILED" +) + +type RewardOperateRecord struct { + ID int64 `xorm:"pk autoincr"` + UserId int64 `xorm:"INDEX NOT NULL"` + Amount int64 `xorm:"NOT NULL"` + RewardType string `xorm:"NOT NULL"` + SourceType string `xorm:"NOT NULL"` + SourceId string `xorm:"INDEX NOT NULL"` + RequestId string `xorm:"INDEX NOT NULL"` + OperateType string `xorm:"NOT NULL"` + CycleIntervalSeconds int64 `xorm:"NOT NULL default 0"` + Status string `xorm:"NOT NULL"` + Remark string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error) { + has, err := x.Get(tl) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRecordNotExist{} + } + return tl, nil +} + +func GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId string) (*RewardOperateRecord, error) { + t := &RewardOperateRecord{ + SourceType: sourceType, + RequestId: requestId, + } + return getPointOperateRecord(t) +} + +func InsertAwardOperateRecord(tl *RewardOperateRecord) (int64, error) { + return x.Insert(tl) +} + +func UpdateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus string) (int64, error) { + r := &RewardOperateRecord{ + Status: newStatus, + } + return x.Cols("status").Where("source_type=? and requestId=? and status=?", sourceType, requestId, oldStatus).Update(r) +} diff --git a/models/task_accomplish_log.go b/models/task_accomplish_log.go index ed2927678..707c214f5 100644 --- a/models/task_accomplish_log.go +++ b/models/task_accomplish_log.go @@ -7,6 +7,7 @@ import ( type TaskAccomplishLog struct { ID int64 `xorm:"pk autoincr"` + LogId string `xorm:"INDEX NOT NULL"` ConfigId int64 `xorm:"NOT NULL"` TaskCode string `xorm:"NOT NULL"` UserId int64 `xorm:"INDEX NOT NULL"` @@ -14,9 +15,10 @@ type TaskAccomplishLog struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` } -type LimiterPeriod struct { +type PeriodResult struct { StartTime time.Time EndTime time.Time + LeftTime time.Duration } func getTaskAccomplishLog(tl *TaskAccomplishLog) (*TaskAccomplishLog, error) { @@ -37,7 +39,7 @@ func GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskCode string) (*Task return getTaskAccomplishLog(t) } -func CountInTaskPeriod(configId int64, userId int64, period *LimiterPeriod) (int64, error) { +func CountInTaskPeriod(configId int64, userId int64, period *PeriodResult) (int64, error) { if period == nil { return x.Where("config_id = ? and user_id = ?", configId, userId).Count(&TaskAccomplishLog{}) } else { diff --git a/models/task_config.go b/models/task_config.go index f74237b59..036f4e315 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -16,8 +16,8 @@ func (t *TaskType) String() string { } const ( - TaskConfigRefreshRateNotCycle = "NOT_CYCLE" - TaskConfigRefreshRateDaily = "DAILY" + PeriodNotCycle = "NOT_CYCLE" + PeriodDaily = "DAILY" ) //PointTaskConfig Only add and delete are allowed, edit is not allowed diff --git a/modules/redis/redis_client/client.go b/modules/redis/redis_client/client.go index 437aecdae..2c487a72c 100644 --- a/modules/redis/redis_client/client.go +++ b/modules/redis/redis_client/client.go @@ -85,3 +85,48 @@ func TTL(key string) (int, error) { return n, nil } + +func IncrBy(key string, n int64) (int64, error) { + redisClient := labelmsg.Get() + defer redisClient.Close() + + reply, err := redisClient.Do("INCRBY", key, n) + if err != nil { + return 0, err + } + i, err := strconv.ParseInt(fmt.Sprint(reply), 10, 64) + return i, nil + +} + +func Expire(key string, expireSeconds int64) error { + redisClient := labelmsg.Get() + defer redisClient.Close() + + _, err := redisClient.Do("EXPIRE ", key, expireSeconds) + if err != nil { + return err + } + return nil + +} + +//GetInt64 get redis value by Get(key) +//and then parse the value to int64 +//return {isExist(bool)} {value(int64)} {error(error)} +func GetInt64(key string) (bool, int64, error) { + str, err := Get(key) + if err != nil { + return false, 0, err + } + if str == "" { + return false, 0, nil + } + + i, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return false, 0, err + } + return true, i, nil + +} diff --git a/modules/redis/redis_key/account_redis_key.go b/modules/redis/redis_key/account_redis_key.go new file mode 100644 index 000000000..f36a8ea5c --- /dev/null +++ b/modules/redis/redis_key/account_redis_key.go @@ -0,0 +1,17 @@ +package redis_key + +import "fmt" + +const ACCOUNT_REDIS_PREFIX = "account" + +func PointAccountOperateLock(accountCode string) string { + return KeyJoin(ACCOUNT_REDIS_PREFIX, accountCode, "operate", "lock") +} + +func PointAccountDetail(userId int64) string { + return KeyJoin(ACCOUNT_REDIS_PREFIX, fmt.Sprint(userId), "detail") +} + +func PointAccountInitLock(userId int64) string { + return KeyJoin(ACCOUNT_REDIS_PREFIX, fmt.Sprint(userId), "init", "lock") +} diff --git a/modules/redis/redis_key/limit_redis_key.go b/modules/redis/redis_key/limit_redis_key.go new file mode 100644 index 000000000..e9d8352a2 --- /dev/null +++ b/modules/redis/redis_key/limit_redis_key.go @@ -0,0 +1,26 @@ +package redis_key + +import ( + "code.gitea.io/gitea/models" + "fmt" +) + +const LIMIT_REDIS_PREFIX = "limit" + +func LimitCount(userId int64, limitCode string, period *models.PeriodResult) string { + if userId == 0 { + if period == nil { + return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, "count") + } + return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") + } + if period == nil { + return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, "count") + } + return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") + +} + +func LimitConfig(limitCode string) string { + return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, "config") +} diff --git a/modules/redis/redis_key/reward_redis_key.go b/modules/redis/redis_key/reward_redis_key.go new file mode 100644 index 000000000..df8c0ca16 --- /dev/null +++ b/modules/redis/redis_key/reward_redis_key.go @@ -0,0 +1,7 @@ +package redis_key + +const REWARD_REDIS_PREFIX = "reward" + +func RewardSendLock(requestId string, sourceType string) string { + return KeyJoin(REWARD_REDIS_PREFIX, requestId, sourceType, "send") +} diff --git a/modules/redis/redis_key/task_redis_key.go b/modules/redis/redis_key/task_redis_key.go index b33e575fb..2eb8c21d1 100644 --- a/modules/redis/redis_key/task_redis_key.go +++ b/modules/redis/redis_key/task_redis_key.go @@ -2,13 +2,12 @@ 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 TaskAccomplishLock(sourceId string, taskType models.TaskType) string { + return KeyJoin(TASK_REDIS_PREFIX, sourceId, taskType.String(), "accomplish") } func TaskConfig(taskType models.TaskType) string { diff --git a/modules/util/uuid_util.go b/modules/util/uuid_util.go new file mode 100644 index 000000000..301c6ff38 --- /dev/null +++ b/modules/util/uuid_util.go @@ -0,0 +1,10 @@ +package util + +import ( + gouuid "github.com/satori/go.uuid" + "strings" +) + +func UUID() string { + return strings.ReplaceAll(gouuid.NewV4().String(), "-", "") +} diff --git a/services/reward/account/account.go b/services/reward/account/account.go deleted file mode 100644 index 4967b368e..000000000 --- a/services/reward/account/account.go +++ /dev/null @@ -1,9 +0,0 @@ -package account - -func IncreaseAmount(userId int64, amount int64) error { - return nil -} - -func DecreaseAmount(userId int64, amount int64) error { - return nil -} diff --git a/services/reward/callback.go b/services/reward/callback.go deleted file mode 100644 index b67ffa673..000000000 --- a/services/reward/callback.go +++ /dev/null @@ -1,4 +0,0 @@ -package reward - -type CallbackHandler struct { -} diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go new file mode 100644 index 000000000..aca8af22e --- /dev/null +++ b/services/reward/limiter/limiter.go @@ -0,0 +1,93 @@ +package limiter + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/services/task/period" + "encoding/json" + "errors" + "fmt" + "time" +) + +type limiterRunner struct { + limiters []models.LimitConfig + index int + userId int64 + amount int64 + limitCode string +} + +func newLimiterRunner(limitCode string, userId, amount int64) *limiterRunner { + return &limiterRunner{ + userId: userId, + amount: amount, + limitCode: limitCode, + index: 0, + } +} + +func (l *limiterRunner) Run() error { + if err := l.LoadLimiters(l.limitCode); err != nil { + return err + } + //todo 验证未配置的情况 + for l.index <= len(l.limiters) { + err := l.limit(l.limiters[l.index]) + if err != nil { + return err + } + l.index += 1 + } + return nil +} + +func (l *limiterRunner) limit(r models.LimitConfig) error { + p, err := period.GetPeriod(r.RefreshRate) + if err != nil { + return err + } + redisKey := redis_key.LimitCount(l.userId, r.LimitCode, p) + usedNum, err := redis_client.IncrBy(redisKey, l.amount) + //if it is the first time,set expire time + if usedNum == l.amount && p != nil { + //todo 验证浮点精确度 + redis_client.Expire(redisKey, int64(p.LeftTime.Seconds())) + } + if usedNum > r.LimitNum { + redis_client.IncrBy(redisKey, -1*l.amount) + return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) + } + return nil +} + +func (l *limiterRunner) LoadLimiters(limitCode string) error { + redisKey := redis_key.LimitConfig(limitCode) + val, _ := redis_client.Get(redisKey) + if val != "" { + if val == redis_key.EMPTY_REDIS_VAL { + return nil + } + limiters := make([]models.LimitConfig, 0) + json.Unmarshal([]byte(val), limiters) + return nil + } + limiters, err := models.GetLimitConfigByLimitCode(limitCode) + if err != nil { + if models.IsErrRecordNotExist(err) { + redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) + return nil + } + return err + } + jsonStr, _ := json.Marshal(limiters) + redis_client.Setex(redisKey, string(jsonStr), 30*24*time.Hour) + + return nil +} + +func CheckLimit(limitCode string, userId, amount int64) error { + r := newLimiterRunner(limitCode, userId, amount) + return r.Run() +} diff --git a/services/reward/operator.go b/services/reward/operator.go index 848ba703d..321562474 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -2,9 +2,13 @@ package reward 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/point" "errors" "fmt" + "time" ) var RewardOperatorMap = map[string]RewardOperator{ @@ -12,47 +16,109 @@ var RewardOperatorMap = map[string]RewardOperator{ } type RewardOperateContext struct { - SourceType models.RewardSourceType - SourceId string - Remark string - Reward Reward - TargetUserId int64 -} - -type RewardOperateResponse int - -const ( - RewardOperateSuccess RewardOperateResponse = iota + 1 - RewardOperateBalanceNotEnough -) - -func (t RewardOperateResponse) IsSuccess() bool { - return t == RewardOperateSuccess + SourceType models.RewardSourceType + SourceId string + Remark string + Reward Reward + TargetUserId int64 + RequestId string + OperateType string + CycleIntervalSeconds int64 } type RewardOperator interface { - IsOperated(ctx RewardOperateContext) bool IsLimited(ctx RewardOperateContext) bool Operate(ctx RewardOperateContext) error } func Send(ctx RewardOperateContext) error { + //add lock + var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardSendLock(ctx.RequestId, ctx.SourceType.String())) + if !rewardLock.Lock(3 * time.Second) { + log.Info("duplicated reward request,targetUserId=%d requestId=%s", ctx.TargetUserId, ctx.RequestId) + return nil + } + defer rewardLock.UnLock() + + //is handled before? + isHandled, err := isHandled(ctx.SourceType, ctx.RequestId) + if err != nil { + log.Error("reward is handled error,%v", err) + return err + } + if isHandled { + log.Info("reward has been handled,ctx=%+v", ctx) + return nil + } + + //get operator operator := GetOperator(ctx.Reward.Type) if operator == nil { return errors.New("operator of reward type is not exist") } - if operator.IsOperated(ctx) { - return nil - } + + //is limited? if operator.IsLimited(ctx) { return nil } + + //new reward operate record + if err := initAwardOperateRecord(ctx); err != nil { + return err + } + + //operate if err := operator.Operate(ctx); err != nil { + updateAwardOperateRecordStatus(ctx.SourceType.String(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) return err } + + //if not a cycle operate,update status to success + if ctx.CycleIntervalSeconds > 0 { + updateAwardOperateRecordStatus(ctx.SourceType.String(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusSucceeded) + } return nil } func GetOperator(rewardType string) RewardOperator { return RewardOperatorMap[rewardType] } + +func isHandled(sourceType models.RewardSourceType, requestId string) (bool, error) { + _, err := models.GetPointOperateRecordBySourceTypeAndRequestId(sourceType.String(), requestId) + if err != nil { + if models.IsErrRecordNotExist(err) { + return false, nil + } + return false, err + } + return true, nil + +} + +func initAwardOperateRecord(ctx RewardOperateContext) error { + _, err := models.InsertAwardOperateRecord(&models.RewardOperateRecord{ + UserId: ctx.TargetUserId, + Amount: ctx.Reward.Amount, + RewardType: ctx.Reward.Type, + SourceType: ctx.SourceType.String(), + SourceId: ctx.SourceId, + RequestId: ctx.RequestId, + OperateType: ctx.OperateType, + CycleIntervalSeconds: ctx.CycleIntervalSeconds, + Status: models.OperateStatusOperating, + Remark: ctx.Remark, + }) + if err != nil { + return err + } + return nil +} + +func updateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus string) error { + _, err := models.UpdateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus) + if err != nil { + return err + } + return nil +} diff --git a/services/reward/point/account/point_account.go b/services/reward/point/account/point_account.go new file mode 100644 index 000000000..9ff5001fc --- /dev/null +++ b/services/reward/point/account/point_account.go @@ -0,0 +1,58 @@ +package account + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/redis/redis_lock" + "code.gitea.io/gitea/modules/util" + "encoding/json" + "time" +) + +func GetAccount(userId int64) (*models.PointAccount, error) { + redisKey := redis_key.PointAccountDetail(userId) + val, _ := redis_client.Get(redisKey) + if val != "" { + account := &models.PointAccount{} + json.Unmarshal([]byte(val), account) + return account, nil + } + account, err := models.GetAccountByUserId(userId) + if err != nil { + if models.IsErrRecordNotExist(err) { + a, err := InitAccount(userId) + if err != nil { + return nil, err + } + return a, nil + } + return nil, err + } + jsonStr, _ := json.Marshal(account) + redis_client.Setex(redisKey, string(jsonStr), 24*time.Hour) + return account, nil +} + +func InitAccount(userId int64) (*models.PointAccount, error) { + lock := redis_lock.NewDistributeLock(redis_key.PointAccountInitLock(userId)) + if lock.LockWithWait(3*time.Second, 3*time.Second) { + defer lock.UnLock() + account, _ := models.GetAccountByUserId(userId) + if account == nil { + models.InsertAccount(&models.PointAccount{ + Balance: 0, + TotalEarned: 0, + TotalConsumed: 0, + UserId: userId, + Status: models.PointAccountNormal, + Version: 0, + AccountCode: util.UUID(), + }) + return models.GetAccountByUserId(userId) + } + return account, nil + } + return nil, nil + +} diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 5a6c18bff..ddcac515b 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -1,19 +1,44 @@ package point import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/redis/redis_lock" "code.gitea.io/gitea/services/reward" + "code.gitea.io/gitea/services/reward/limiter" + "code.gitea.io/gitea/services/reward/point/account" + "errors" + "time" ) type PointOperator struct { } -func (operator *PointOperator) IsOperated(ctx reward.RewardOperateContext) bool { - //todo - return true -} func (operator *PointOperator) IsLimited(ctx reward.RewardOperateContext) bool { + if err := limiter.CheckLimit(ctx.Reward.Type, ctx.TargetUserId, ctx.Reward.Amount); err != nil { + return false + } return true } + func (operator *PointOperator) Operate(ctx reward.RewardOperateContext) error { + a, err := account.GetAccount(ctx.TargetUserId) + if err != nil || a == nil { + return errors.New("get account error") + } + + lock := redis_lock.NewDistributeLock(redis_key.PointAccountOperateLock(a.AccountCode)) + if lock.LockWithWait(3*time.Second, 3*time.Second) { + defer lock.UnLock() + na, _ := account.GetAccount(ctx.TargetUserId) + if ctx.OperateType == models.OperateTypeIncrease { + na.Increase(ctx.Reward.Amount, ctx.SourceId) + } else if ctx.OperateType == models.OperateTypeDecrease { + na.Decrease(ctx.Reward.Amount, ctx.SourceId) + } + + } else { + return errors.New("Get account operate lock failed") + } return nil } diff --git a/services/task/limiter.go b/services/task/limiter.go deleted file mode 100644 index 6c2cd4f44..000000000 --- a/services/task/limiter.go +++ /dev/null @@ -1,39 +0,0 @@ -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/period/handler.go b/services/task/period/handler.go new file mode 100644 index 000000000..c3e5443d3 --- /dev/null +++ b/services/task/period/handler.go @@ -0,0 +1,50 @@ +package period + +import ( + "code.gitea.io/gitea/models" + "errors" + "time" +) + +var PeriodHandlerMap = map[string]PeriodHandler{ + models.PeriodNotCycle: new(NoCycleHandler), + models.PeriodDaily: new(DailyHandler), +} + +type PeriodHandler interface { + GetCurrentPeriod() *models.PeriodResult +} + +type NoCycleHandler struct { +} + +func (l *NoCycleHandler) GetCurrentPeriod() *models.PeriodResult { + return nil +} + +type DailyHandler struct { +} + +func (l *DailyHandler) GetCurrentPeriod() *models.PeriodResult { + t := time.Now() + startTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + endTime := startTime.Add(24 * time.Hour) + leftTime := endTime.Sub(t) + return &models.PeriodResult{ + StartTime: startTime, + EndTime: endTime, + LeftTime: leftTime, + } +} + +func getPeriodHandler(refreshRateype string) PeriodHandler { + return PeriodHandlerMap[refreshRateype] +} + +func GetPeriod(refreshRate string) (*models.PeriodResult, error) { + handler := getPeriodHandler(refreshRate) + if handler == nil { + return nil, errors.New("task config incorrect") + } + return handler.GetCurrentPeriod(), nil +} diff --git a/services/task/task.go b/services/task/task.go index c5f65240b..403a2ba8f 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -5,8 +5,9 @@ import ( "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/modules/util" "code.gitea.io/gitea/services/reward" - "errors" + "code.gitea.io/gitea/services/task/period" "time" ) @@ -16,7 +17,7 @@ func Accomplish(userId int64, taskType models.TaskType, sourceId string) { func accomplish(userId int64, taskType models.TaskType, sourceId string) error { //lock - var taskLock = redis_lock.NewDistributeLock(redis_key.TaskAccomplishLock(userId, sourceId, taskType)) + var taskLock = redis_lock.NewDistributeLock(redis_key.TaskAccomplishLock(sourceId, taskType)) if !taskLock.Lock(3 * time.Second) { log.Info("duplicated task request,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) return nil @@ -57,12 +58,17 @@ func accomplish(userId int64, taskType models.TaskType, sourceId string) error { } //add log - models.InsertTaskAccomplishLog(&models.TaskAccomplishLog{ + logId := util.UUID() + _, err = models.InsertTaskAccomplishLog(&models.TaskAccomplishLog{ + LogId: logId, ConfigId: config.ID, TaskCode: config.TaskCode, UserId: userId, SourceId: sourceId, }) + if err != nil { + return err + } //reward reward.Send(reward.RewardOperateContext{ @@ -73,6 +79,7 @@ func accomplish(userId int64, taskType models.TaskType, sourceId string) error { Type: config.AwardType, }, TargetUserId: userId, + RequestId: logId, }) return nil @@ -91,11 +98,11 @@ func isHandled(taskType models.TaskType, sourceId string) (bool, error) { } func IsLimited(userId int64, config *models.TaskConfig) (bool, error) { - limiter := GetLimiter(config.RefreshRate) - if limiter == nil { - return false, errors.New("task config incorrect") + p, err := period.GetPeriod(config.RefreshRate) + if err != nil { + return false, err } - n, err := models.CountInTaskPeriod(config.ID, userId, limiter.GetCurrentPeriod()) + n, err := models.CountInTaskPeriod(config.ID, userId, p) if err != nil { return false, err } diff --git a/services/task/task_config.go b/services/task/task_config.go index 6812f5d67..22e0b1828 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -24,9 +24,9 @@ func GetTaskConfig(taskType models.TaskType) (*models.TaskConfig, error) { config, err := models.GetTaskConfigByTaskCode(taskType.String()) if err != nil { if models.IsErrRecordNotExist(err) { + redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) return nil, nil } - redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) return nil, err } jsonStr, _ := json.Marshal(config) From 0a4ab4dcaf60018a2e5dcbebc272ad50aea3fc89 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 2 Jun 2022 18:02:14 +0800 Subject: [PATCH 005/283] update --- models/limit_config.go | 30 +++++-- models/models.go | 7 ++ models/point_account.go | 6 +- models/repo_watch.go | 2 +- models/reward_operate_record.go | 31 ++++--- models/task_config.go | 18 ++-- modules/auth/wechat/access_token.go | 8 +- modules/notification/notification.go | 2 + modules/notification/task/task.go | 85 +++++++++++++++++++ modules/redis/redis_client/client.go | 2 +- modules/redis/redis_key/account_redis_key.go | 4 +- modules/redis/redis_key/limit_redis_key.go | 4 +- modules/redis/redis_key/task_redis_key.go | 12 +-- modules/redis/redis_lock/lock.go | 22 +++-- services/reward/limiter/limiter.go | 50 +++++++---- services/reward/operator.go | 61 +++++++------ .../reward/point/account/point_account.go | 8 +- services/reward/point/point_operate.go | 26 ++++-- services/reward/reward.go | 6 -- services/task/task.go | 51 +++++------ services/task/task_config.go | 4 +- 21 files changed, 292 insertions(+), 147 deletions(-) create mode 100644 modules/notification/task/task.go delete mode 100644 services/reward/reward.go diff --git a/models/limit_config.go b/models/limit_config.go index 273af0de1..2196b5b6d 100644 --- a/models/limit_config.go +++ b/models/limit_config.go @@ -2,6 +2,24 @@ package models import "code.gitea.io/gitea/modules/timeutil" +type LimitType string + +const ( + LimitTypeTask LimitType = "TASK" + LimitTypeReward LimitType = "REWARD" +) + +func (l LimitType) Name() string { + switch l { + case LimitTypeTask: + return "TASK" + case LimitTypeReward: + return "REWARD" + default: + return "" + } +} + type LimitConfig struct { ID int64 `xorm:"pk autoincr"` Tittle string @@ -9,14 +27,15 @@ type LimitConfig struct { Scope string `xorm:"NOT NULL"` LimitNum int64 `xorm:"NOT NULL"` LimitCode string `xorm:"NOT NULL"` + LimitType string `xorm:"NOT NULL"` Creator int64 `xorm:"NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"created"` DeletedAt timeutil.TimeStamp `xorm:"deleted"` } -func findLimitConfig(tl *LimitConfig) ([]LimitConfig, error) { +func GetLimitConfigByLimitCode(limitCode string, limitType LimitType) ([]LimitConfig, error) { r := make([]LimitConfig, 0) - err := x.Find(r, tl) + err := x.Where("limit_code = ? and limit_type = ?", limitCode, limitType.Name()).Find(&r) if err != nil { return nil, err } else if len(r) == 0 { @@ -24,10 +43,3 @@ func findLimitConfig(tl *LimitConfig) ([]LimitConfig, error) { } return r, nil } - -func GetLimitConfigByLimitCode(limitCode string) ([]LimitConfig, error) { - t := &LimitConfig{ - LimitCode: limitCode, - } - return findLimitConfig(t) -} diff --git a/models/models.go b/models/models.go index 9d255c5e6..59e7a3a48 100755 --- a/models/models.go +++ b/models/models.go @@ -144,6 +144,13 @@ func init() { new(WechatBindLog), new(OrgStatistic), new(SearchRecord), + new(TaskConfig), + new(TaskAccomplishLog), + new(RewardOperateRecord), + new(LimitConfig), + new(PeriodicTask), + new(PointAccountLog), + new(PointAccount), ) tablesStatistic = append(tablesStatistic, diff --git a/models/point_account.go b/models/point_account.go index 7fa38cb7a..9a8032553 100644 --- a/models/point_account.go +++ b/models/point_account.go @@ -1,6 +1,8 @@ package models -import "code.gitea.io/gitea/modules/timeutil" +import ( + "code.gitea.io/gitea/modules/timeutil" +) type PointAccountStatus int @@ -87,7 +89,7 @@ func GetAccountByUserId(userId int64) (*PointAccount, error) { return nil, err } if !has { - return nil, nil + return nil, ErrRecordNotExist{} } return p, nil } diff --git a/models/repo_watch.go b/models/repo_watch.go index 31868fcae..2d01bde1f 100644 --- a/models/repo_watch.go +++ b/models/repo_watch.go @@ -287,7 +287,7 @@ func NotifyWatchers(actions ...*Action) error { func producer(actions ...*Action) { for _, action := range actions { - if !action.IsPrivate{ + if !action.IsPrivate { ActionChan <- action } } diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index ca1f52168..6f4e9a797 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -5,18 +5,12 @@ import ( "fmt" ) -type RewardSourceType string - const ( - SourceTypeAccomplishTask RewardSourceType = "ACCOMPLISH_TASK" - SourceTypeAdminOperate RewardSourceType = "ADMIN_OPERATE" - SourceTypeRunCloudbrainTask RewardSourceType = "RUN_CLOUBRAIN_TASK" + SourceTypeAccomplishTask string = "ACCOMPLISH_TASK" + SourceTypeAdminOperate = "ADMIN_OPERATE" + SourceTypeRunCloudbrainTask = "RUN_CLOUBRAIN_TASK" ) -func (r *RewardSourceType) String() string { - return fmt.Sprint(r) -} - type RewardType string const ( @@ -40,6 +34,7 @@ const ( type RewardOperateRecord struct { ID int64 `xorm:"pk autoincr"` + RecordId string `xorm:"INDEX NOT NULL"` UserId int64 `xorm:"INDEX NOT NULL"` Amount int64 `xorm:"NOT NULL"` RewardType string `xorm:"NOT NULL"` @@ -80,5 +75,21 @@ func UpdateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus r := &RewardOperateRecord{ Status: newStatus, } - return x.Cols("status").Where("source_type=? and requestId=? and status=?", sourceType, requestId, oldStatus).Update(r) + return x.Cols("status").Where("source_type=? and request_id=? and status=?", sourceType, requestId, oldStatus).Update(r) +} + +type RewardOperateContext struct { + SourceType string + SourceId string + Remark string + Reward Reward + TargetUserId int64 + RequestId string + OperateType string + CycleIntervalSeconds int64 +} + +type Reward struct { + Amount int64 + Type string } diff --git a/models/task_config.go b/models/task_config.go index 036f4e315..aa09ee603 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -2,19 +2,13 @@ package models import ( "code.gitea.io/gitea/modules/timeutil" - "fmt" ) -type TaskType string - const ( - TaskTypeComment TaskType = "COMMENT" + TaskTypeCreateIssueComment string = "CREATE_IS" + TaskTypeNewIssue = "NEW_ISSUE" ) -func (t *TaskType) String() string { - return fmt.Sprint(t) -} - const ( PeriodNotCycle = "NOT_CYCLE" PeriodDaily = "DAILY" @@ -23,11 +17,9 @@ const ( //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"` + ID int64 `xorm:"pk autoincr"` + TaskCode string `xorm:"NOT NULL"` + Tittle string AwardType string `xorm:"NOT NULL"` AwardAmount int64 `xorm:"NOT NULL"` Creator int64 `xorm:"NOT NULL"` diff --git a/modules/auth/wechat/access_token.go b/modules/auth/wechat/access_token.go index af62c3e7b..e4e38ee30 100644 --- a/modules/auth/wechat/access_token.go +++ b/modules/auth/wechat/access_token.go @@ -26,14 +26,18 @@ func GetWechatAccessToken() string { } func refreshAccessToken() { - if ok := accessTokenLock.Lock(3 * time.Second); ok { + if ok, _ := accessTokenLock.Lock(3 * time.Second); ok { defer accessTokenLock.UnLock() callAccessTokenAndUpdateCache() } } func refreshAndGetAccessToken() string { - if ok := accessTokenLock.LockWithWait(3*time.Second, 3*time.Second); ok { + isOk, err := accessTokenLock.LockWithWait(3*time.Second, 3*time.Second) + if err != nil { + return "" + } + if isOk { defer accessTokenLock.UnLock() token, _ := redis_client.Get(redis_key.WechatAccessTokenKey()) if token != "" { diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 0fd6fa471..8329ca903 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/modules/notification/base" "code.gitea.io/gitea/modules/notification/indexer" "code.gitea.io/gitea/modules/notification/mail" + "code.gitea.io/gitea/modules/notification/task" "code.gitea.io/gitea/modules/notification/ui" "code.gitea.io/gitea/modules/notification/webhook" "code.gitea.io/gitea/modules/repository" @@ -35,6 +36,7 @@ func NewContext() { RegisterNotifier(indexer.NewNotifier()) RegisterNotifier(webhook.NewNotifier()) RegisterNotifier(action.NewNotifier()) + RegisterNotifier(task.NewNotifier()) } // NotifyUploadAttachment notifies attachment upload message to notifiers diff --git a/modules/notification/task/task.go b/modules/notification/task/task.go new file mode 100644 index 000000000..ce3b023ba --- /dev/null +++ b/modules/notification/task/task.go @@ -0,0 +1,85 @@ +package task + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/notification/base" + "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/services/task" + "fmt" +) + +type taskNotifier struct { + base.NullNotifier +} + +var ( + _ base.Notifier = &taskNotifier{} +) + +// NewNotifier create a new actionNotifier notifier +func NewNotifier() base.Notifier { + return &taskNotifier{} +} + +func (t *taskNotifier) NotifyNewIssue(issue *models.Issue) { + task.Accomplish(issue.Poster.ID, models.TaskTypeNewIssue, fmt.Sprint(issue.ID)) +} + +// NotifyIssueChangeStatus notifies close or reopen issue to notifiers +func (t *taskNotifier) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) { + return +} + +// NotifyCreateIssueComment notifies comment on an issue to notifiers +func (t *taskNotifier) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, + issue *models.Issue, comment *models.Comment) { + task.Accomplish(doer.ID, models.TaskTypeCreateIssueComment, fmt.Sprint(comment.ID)) +} + +func (t *taskNotifier) NotifyNewPullRequest(pull *models.PullRequest) { + task.Accomplish(pull.Issue.Poster.ID, models.TaskTypeCreateIssueComment, fmt.Sprint(pull.ID)) +} + +func (t *taskNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldRepoName string) { + return +} + +func (t *taskNotifier) NotifyAliasRepository(doer *models.User, repo *models.Repository, oldAlias string) { + return +} + +func (t *taskNotifier) NotifyTransferRepository(doer *models.User, repo *models.Repository, oldOwnerName string) { + return +} + +func (t *taskNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { + return +} + +func (t *taskNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { + return +} + +func (t *taskNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment) { + return +} + +func (t *taskNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User) { + return +} + +func (t *taskNotifier) NotifySyncPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) { + return +} + +func (t *taskNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) { + return +} + +func (t *taskNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) { + return +} + +func (t *taskNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) { + return +} diff --git a/modules/redis/redis_client/client.go b/modules/redis/redis_client/client.go index 2c487a72c..21a6da9fb 100644 --- a/modules/redis/redis_client/client.go +++ b/modules/redis/redis_client/client.go @@ -103,7 +103,7 @@ func Expire(key string, expireSeconds int64) error { redisClient := labelmsg.Get() defer redisClient.Close() - _, err := redisClient.Do("EXPIRE ", key, expireSeconds) + _, err := redisClient.Do("EXPIRE", key, expireSeconds) if err != nil { return err } diff --git a/modules/redis/redis_key/account_redis_key.go b/modules/redis/redis_key/account_redis_key.go index f36a8ea5c..896ea4ff4 100644 --- a/modules/redis/redis_key/account_redis_key.go +++ b/modules/redis/redis_key/account_redis_key.go @@ -8,8 +8,8 @@ func PointAccountOperateLock(accountCode string) string { return KeyJoin(ACCOUNT_REDIS_PREFIX, accountCode, "operate", "lock") } -func PointAccountDetail(userId int64) string { - return KeyJoin(ACCOUNT_REDIS_PREFIX, fmt.Sprint(userId), "detail") +func PointAccountInfo(userId int64) string { + return KeyJoin(ACCOUNT_REDIS_PREFIX, fmt.Sprint(userId), "info") } func PointAccountInitLock(userId int64) string { diff --git a/modules/redis/redis_key/limit_redis_key.go b/modules/redis/redis_key/limit_redis_key.go index e9d8352a2..86a77e59e 100644 --- a/modules/redis/redis_key/limit_redis_key.go +++ b/modules/redis/redis_key/limit_redis_key.go @@ -21,6 +21,6 @@ func LimitCount(userId int64, limitCode string, period *models.PeriodResult) str } -func LimitConfig(limitCode string) string { - return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, "config") +func LimitConfig(limitCode string, limitType models.LimitType) string { + return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, limitType.Name(), "config") } diff --git a/modules/redis/redis_key/task_redis_key.go b/modules/redis/redis_key/task_redis_key.go index 2eb8c21d1..3427c8f7f 100644 --- a/modules/redis/redis_key/task_redis_key.go +++ b/modules/redis/redis_key/task_redis_key.go @@ -1,15 +1,11 @@ package redis_key -import ( - "code.gitea.io/gitea/models" -) - const TASK_REDIS_PREFIX = "task" -func TaskAccomplishLock(sourceId string, taskType models.TaskType) string { - return KeyJoin(TASK_REDIS_PREFIX, sourceId, taskType.String(), "accomplish") +func TaskAccomplishLock(sourceId string, taskType string) string { + return KeyJoin(TASK_REDIS_PREFIX, sourceId, taskType, "accomplish") } -func TaskConfig(taskType models.TaskType) string { - return KeyJoin(TASK_REDIS_PREFIX, "config", taskType.String()) +func TaskConfig(taskType string) string { + return KeyJoin(TASK_REDIS_PREFIX, "config", taskType) } diff --git a/modules/redis/redis_lock/lock.go b/modules/redis/redis_lock/lock.go index b8cd837f1..5723c379d 100644 --- a/modules/redis/redis_lock/lock.go +++ b/modules/redis/redis_lock/lock.go @@ -13,26 +13,32 @@ func NewDistributeLock(lockKey string) *DistributeLock { return &DistributeLock{lockKey: lockKey} } -func (lock *DistributeLock) Lock(expireTime time.Duration) bool { - isOk, _ := redis_client.Setnx(lock.lockKey, "", expireTime) - return isOk +func (lock *DistributeLock) Lock(expireTime time.Duration) (bool, error) { + isOk, err := redis_client.Setnx(lock.lockKey, "", expireTime) + if err != nil { + return false, err + } + return isOk, nil } -func (lock *DistributeLock) LockWithWait(expireTime time.Duration, waitTime time.Duration) bool { +func (lock *DistributeLock) LockWithWait(expireTime time.Duration, waitTime time.Duration) (bool, error) { start := time.Now().Unix() * 1000 duration := waitTime.Milliseconds() for { - isOk, _ := redis_client.Setnx(lock.lockKey, "", expireTime) + isOk, err := redis_client.Setnx(lock.lockKey, "", expireTime) + if err != nil { + return false, err + } if isOk { - return true + return true, nil } if time.Now().Unix()*1000-start > duration { - return false + return false, nil } time.Sleep(50 * time.Millisecond) } - return false + return false, nil } func (lock *DistributeLock) UnLock() error { diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go index aca8af22e..8117ba173 100644 --- a/services/reward/limiter/limiter.go +++ b/services/reward/limiter/limiter.go @@ -2,6 +2,7 @@ package limiter import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/services/task/period" @@ -17,25 +18,27 @@ type limiterRunner struct { userId int64 amount int64 limitCode string + limitType models.LimitType } -func newLimiterRunner(limitCode string, userId, amount int64) *limiterRunner { +func newLimiterRunner(limitCode string, limitType models.LimitType, userId, amount int64) *limiterRunner { return &limiterRunner{ userId: userId, amount: amount, limitCode: limitCode, + limitType: limitType, index: 0, } } func (l *limiterRunner) Run() error { - if err := l.LoadLimiters(l.limitCode); err != nil { + if err := l.LoadLimiters(); err != nil { return err } - //todo 验证未配置的情况 - for l.index <= len(l.limiters) { + for l.index < len(l.limiters) { err := l.limit(l.limiters[l.index]) if err != nil { + log.Info("limiter check failed,%v", err) return err } l.index += 1 @@ -62,32 +65,43 @@ func (l *limiterRunner) limit(r models.LimitConfig) error { return nil } -func (l *limiterRunner) LoadLimiters(limitCode string) error { - redisKey := redis_key.LimitConfig(limitCode) +func (l *limiterRunner) LoadLimiters() error { + limiters, err := GetLimiters(l.limitCode, l.limitType) + if err != nil { + return err + } + if limiters != nil { + l.limiters = limiters + } + return nil +} + +func CheckLimit(limitCode string, limitType models.LimitType, userId, amount int64) error { + r := newLimiterRunner(limitCode, limitType, userId, amount) + return r.Run() +} + +func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitConfig, error) { + redisKey := redis_key.LimitConfig(limitCode, limitType) val, _ := redis_client.Get(redisKey) if val != "" { if val == redis_key.EMPTY_REDIS_VAL { - return nil + return nil, nil } limiters := make([]models.LimitConfig, 0) - json.Unmarshal([]byte(val), limiters) - return nil + json.Unmarshal([]byte(val), &limiters) + return limiters, nil } - limiters, err := models.GetLimitConfigByLimitCode(limitCode) + limiters, err := models.GetLimitConfigByLimitCode(limitCode, limitType) if err != nil { if models.IsErrRecordNotExist(err) { redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) - return nil + return nil, nil } - return err + return nil, err } jsonStr, _ := json.Marshal(limiters) redis_client.Setex(redisKey, string(jsonStr), 30*24*time.Hour) - return nil -} - -func CheckLimit(limitCode string, userId, amount int64) error { - r := newLimiterRunner(limitCode, userId, amount) - return r.Run() + return limiters, nil } diff --git a/services/reward/operator.go b/services/reward/operator.go index 321562474..b0bd53f8a 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -5,6 +5,7 @@ import ( "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/modules/util" "code.gitea.io/gitea/services/reward/point" "errors" "fmt" @@ -15,26 +16,25 @@ var RewardOperatorMap = map[string]RewardOperator{ fmt.Sprint(models.RewardTypePoint): new(point.PointOperator), } -type RewardOperateContext struct { - SourceType models.RewardSourceType - SourceId string - Remark string - Reward Reward - TargetUserId int64 - RequestId string - OperateType string - CycleIntervalSeconds int64 -} - type RewardOperator interface { - IsLimited(ctx RewardOperateContext) bool - Operate(ctx RewardOperateContext) error + IsLimited(ctx models.RewardOperateContext) bool + Operate(ctx models.RewardOperateContext) error } -func Send(ctx RewardOperateContext) error { +func Send(ctx models.RewardOperateContext) error { + defer func() { + if err := recover(); err != nil { + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC:%v", combinedErr) + } + }() //add lock - var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardSendLock(ctx.RequestId, ctx.SourceType.String())) - if !rewardLock.Lock(3 * time.Second) { + var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardSendLock(ctx.RequestId, ctx.SourceType)) + isOk, err := rewardLock.Lock(3 * time.Second) + if err != nil { + return err + } + if !isOk { log.Info("duplicated reward request,targetUserId=%d requestId=%s", ctx.TargetUserId, ctx.RequestId) return nil } @@ -63,19 +63,22 @@ func Send(ctx RewardOperateContext) error { } //new reward operate record - if err := initAwardOperateRecord(ctx); err != nil { + recordId, err := initAwardOperateRecord(ctx) + if err != nil { return err } + ctx.SourceId = recordId + //operate if err := operator.Operate(ctx); err != nil { - updateAwardOperateRecordStatus(ctx.SourceType.String(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) + updateAwardOperateRecordStatus(ctx.SourceType, ctx.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) return err } //if not a cycle operate,update status to success - if ctx.CycleIntervalSeconds > 0 { - updateAwardOperateRecordStatus(ctx.SourceType.String(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusSucceeded) + if ctx.CycleIntervalSeconds == 0 { + updateAwardOperateRecordStatus(ctx.SourceType, ctx.RequestId, models.OperateStatusOperating, models.OperateStatusSucceeded) } return nil } @@ -84,8 +87,8 @@ func GetOperator(rewardType string) RewardOperator { return RewardOperatorMap[rewardType] } -func isHandled(sourceType models.RewardSourceType, requestId string) (bool, error) { - _, err := models.GetPointOperateRecordBySourceTypeAndRequestId(sourceType.String(), requestId) +func isHandled(sourceType string, requestId string) (bool, error) { + _, err := models.GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId) if err != nil { if models.IsErrRecordNotExist(err) { return false, nil @@ -96,23 +99,25 @@ func isHandled(sourceType models.RewardSourceType, requestId string) (bool, erro } -func initAwardOperateRecord(ctx RewardOperateContext) error { - _, err := models.InsertAwardOperateRecord(&models.RewardOperateRecord{ +func initAwardOperateRecord(ctx models.RewardOperateContext) (string, error) { + record := &models.RewardOperateRecord{ + RecordId: util.UUID(), UserId: ctx.TargetUserId, Amount: ctx.Reward.Amount, RewardType: ctx.Reward.Type, - SourceType: ctx.SourceType.String(), + SourceType: ctx.SourceType, SourceId: ctx.SourceId, RequestId: ctx.RequestId, OperateType: ctx.OperateType, CycleIntervalSeconds: ctx.CycleIntervalSeconds, Status: models.OperateStatusOperating, Remark: ctx.Remark, - }) + } + _, err := models.InsertAwardOperateRecord(record) if err != nil { - return err + return "", err } - return nil + return record.RecordId, nil } func updateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus string) error { diff --git a/services/reward/point/account/point_account.go b/services/reward/point/account/point_account.go index 9ff5001fc..ea127e162 100644 --- a/services/reward/point/account/point_account.go +++ b/services/reward/point/account/point_account.go @@ -11,7 +11,7 @@ import ( ) func GetAccount(userId int64) (*models.PointAccount, error) { - redisKey := redis_key.PointAccountDetail(userId) + redisKey := redis_key.PointAccountInfo(userId) val, _ := redis_client.Get(redisKey) if val != "" { account := &models.PointAccount{} @@ -36,7 +36,11 @@ func GetAccount(userId int64) (*models.PointAccount, error) { func InitAccount(userId int64) (*models.PointAccount, error) { lock := redis_lock.NewDistributeLock(redis_key.PointAccountInitLock(userId)) - if lock.LockWithWait(3*time.Second, 3*time.Second) { + isOk, err := lock.LockWithWait(3*time.Second, 3*time.Second) + if err != nil { + return nil, err + } + if isOk { defer lock.UnLock() account, _ := models.GetAccountByUserId(userId) if account == nil { diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index ddcac515b..eeba83ac7 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -2,9 +2,9 @@ package point import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/modules/redis/redis_lock" - "code.gitea.io/gitea/services/reward" "code.gitea.io/gitea/services/reward/limiter" "code.gitea.io/gitea/services/reward/point/account" "errors" @@ -14,28 +14,36 @@ import ( type PointOperator struct { } -func (operator *PointOperator) IsLimited(ctx reward.RewardOperateContext) bool { - if err := limiter.CheckLimit(ctx.Reward.Type, ctx.TargetUserId, ctx.Reward.Amount); err != nil { - return false +func (operator *PointOperator) IsLimited(ctx models.RewardOperateContext) bool { + if err := limiter.CheckLimit(ctx.Reward.Type, models.LimitTypeReward, ctx.TargetUserId, ctx.Reward.Amount); err != nil { + return true } - return true + return false } -func (operator *PointOperator) Operate(ctx reward.RewardOperateContext) error { +func (operator *PointOperator) Operate(ctx models.RewardOperateContext) error { a, err := account.GetAccount(ctx.TargetUserId) if err != nil || a == nil { return errors.New("get account error") } lock := redis_lock.NewDistributeLock(redis_key.PointAccountOperateLock(a.AccountCode)) - if lock.LockWithWait(3*time.Second, 3*time.Second) { + isOk, err := lock.LockWithWait(3*time.Second, 3*time.Second) + if err != nil { + return err + } + if isOk { defer lock.UnLock() na, _ := account.GetAccount(ctx.TargetUserId) if ctx.OperateType == models.OperateTypeIncrease { - na.Increase(ctx.Reward.Amount, ctx.SourceId) + err = na.Increase(ctx.Reward.Amount, ctx.SourceId) } else if ctx.OperateType == models.OperateTypeDecrease { - na.Decrease(ctx.Reward.Amount, ctx.SourceId) + err = na.Decrease(ctx.Reward.Amount, ctx.SourceId) + } + if err != nil { + return err } + redis_client.Del(redis_key.PointAccountInfo(ctx.TargetUserId)) } else { return errors.New("Get account operate lock failed") diff --git a/services/reward/reward.go b/services/reward/reward.go deleted file mode 100644 index ca1c1f3cd..000000000 --- a/services/reward/reward.go +++ /dev/null @@ -1,6 +0,0 @@ -package reward - -type Reward struct { - Amount int64 - Type string -} diff --git a/services/task/task.go b/services/task/task.go index 403a2ba8f..f38793419 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -7,18 +7,30 @@ import ( "code.gitea.io/gitea/modules/redis/redis_lock" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/reward" - "code.gitea.io/gitea/services/task/period" + "code.gitea.io/gitea/services/reward/limiter" + "fmt" "time" ) -func Accomplish(userId int64, taskType models.TaskType, sourceId string) { +func Accomplish(userId int64, taskType string, sourceId string) { go accomplish(userId, taskType, sourceId) } -func accomplish(userId int64, taskType models.TaskType, sourceId string) error { +func accomplish(userId int64, taskType string, sourceId string) error { + defer func() { + if err := recover(); err != nil { + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC:%v", combinedErr) + } + }() //lock var taskLock = redis_lock.NewDistributeLock(redis_key.TaskAccomplishLock(sourceId, taskType)) - if !taskLock.Lock(3 * time.Second) { + isOk, err := taskLock.Lock(3 * time.Second) + if err != nil { + log.Error("get taskLock error. %v", err) + return err + } + if !isOk { log.Info("duplicated task request,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) return nil } @@ -47,12 +59,7 @@ func accomplish(userId int64, taskType models.TaskType, sourceId string) error { } //is limited? - isLimited, err := IsLimited(userId, config) - if err != nil { - log.Error("get limited error,%v", err) - return err - } - if isLimited { + if isLimited(userId, config) { log.Info("task accomplish maximum times are reached,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) return nil } @@ -71,22 +78,23 @@ func accomplish(userId int64, taskType models.TaskType, sourceId string) error { } //reward - reward.Send(reward.RewardOperateContext{ + reward.Send(models.RewardOperateContext{ SourceType: models.SourceTypeAccomplishTask, - SourceId: sourceId, - Reward: reward.Reward{ + SourceId: logId, + Reward: models.Reward{ Amount: config.AwardAmount, Type: config.AwardType, }, TargetUserId: userId, RequestId: logId, + OperateType: models.OperateTypeIncrease, }) return nil } -func isHandled(taskType models.TaskType, sourceId string) (bool, error) { - _, err := models.GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskType.String()) +func isHandled(taskType string, sourceId string) (bool, error) { + _, err := models.GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskType) if err != nil { if models.IsErrRecordNotExist(err) { return false, nil @@ -97,15 +105,10 @@ func isHandled(taskType models.TaskType, sourceId string) (bool, error) { } -func IsLimited(userId int64, config *models.TaskConfig) (bool, error) { - p, err := period.GetPeriod(config.RefreshRate) - if err != nil { - return false, err - } - n, err := models.CountInTaskPeriod(config.ID, userId, p) - if err != nil { - return false, err +func isLimited(userId int64, config *models.TaskConfig) bool { + if err := limiter.CheckLimit(config.TaskCode, models.LimitTypeTask, userId, 1); err != nil { + return true } - return n >= config.Times, nil + return false } diff --git a/services/task/task_config.go b/services/task/task_config.go index 22e0b1828..6e7f22e14 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -10,7 +10,7 @@ import ( //GetTaskConfig get task config from redis cache first // if not exist in redis, find in db and refresh the redis key -func GetTaskConfig(taskType models.TaskType) (*models.TaskConfig, error) { +func GetTaskConfig(taskType string) (*models.TaskConfig, error) { redisKey := redis_key.TaskConfig(taskType) configStr, _ := redis_client.Get(redisKey) if configStr != "" { @@ -21,7 +21,7 @@ func GetTaskConfig(taskType models.TaskType) (*models.TaskConfig, error) { json.Unmarshal([]byte(configStr), config) return config, nil } - config, err := models.GetTaskConfigByTaskCode(taskType.String()) + config, err := models.GetTaskConfigByTaskCode(taskType) if err != nil { if models.IsErrRecordNotExist(err) { redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) From c5a35c5982f8de551fc8c5a3f50f13f01e856638 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Mon, 6 Jun 2022 18:08:14 +0800 Subject: [PATCH 006/283] #1249 update limiters --- models/limit_config.go | 38 +++++++++--- models/reward_operate_record.go | 10 ++- models/task_accomplish_log.go | 11 +--- models/task_config.go | 20 +++++- modules/notification/action/action.go | 4 ++ modules/notification/base/notifier.go | 1 + modules/notification/base/null.go | 4 ++ modules/notification/notification.go | 7 +++ modules/notification/task/task.go | 52 +++++++++++----- modules/redis/redis_key/limit_redis_key.go | 16 ++--- services/reward/limiter/limiter.go | 71 +++++++++++++++++++--- services/reward/operator.go | 2 +- services/reward/point/point_operate.go | 2 +- services/task/task.go | 49 ++------------- 14 files changed, 185 insertions(+), 102 deletions(-) diff --git a/models/limit_config.go b/models/limit_config.go index 2196b5b6d..aec26a036 100644 --- a/models/limit_config.go +++ b/models/limit_config.go @@ -5,16 +5,34 @@ import "code.gitea.io/gitea/modules/timeutil" type LimitType string const ( - LimitTypeTask LimitType = "TASK" - LimitTypeReward LimitType = "REWARD" + LimitTypeTask LimitType = "TASK" + LimitTypeRewardPoint LimitType = "REWARD_POINT" ) func (l LimitType) Name() string { switch l { case LimitTypeTask: return "TASK" - case LimitTypeReward: - return "REWARD" + case LimitTypeRewardPoint: + return "REWARD_POINT" + default: + return "" + } +} + +type LimitScope string + +const ( + LimitScopeAllUsers LimitScope = "ALL_USERS" + LimitScopeSingleUser LimitScope = "SINGLE_USER" +) + +func (l LimitScope) Name() string { + switch l { + case LimitScopeAllUsers: + return "ALL_USERS" + case LimitScopeSingleUser: + return "SINGLE_USER" default: return "" } @@ -23,19 +41,19 @@ func (l LimitType) Name() string { type LimitConfig struct { ID int64 `xorm:"pk autoincr"` Tittle string - RefreshRate string `xorm:"NOT NULL"` - Scope string `xorm:"NOT NULL"` - LimitNum int64 `xorm:"NOT NULL"` - LimitCode string `xorm:"NOT NULL"` + RefreshRate string `xorm:"NOT NULL"` + Scope string `xorm:"NOT NULL"` + LimitNum int64 `xorm:"NOT NULL"` + LimitCode string LimitType string `xorm:"NOT NULL"` Creator int64 `xorm:"NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"created"` DeletedAt timeutil.TimeStamp `xorm:"deleted"` } -func GetLimitConfigByLimitCode(limitCode string, limitType LimitType) ([]LimitConfig, error) { +func GetLimitConfigByLimitType(limitType LimitType) ([]LimitConfig, error) { r := make([]LimitConfig, 0) - err := x.Where("limit_code = ? and limit_type = ?", limitCode, limitType.Name()).Find(&r) + err := x.Where(" limit_type = ?", limitType.Name()).Find(&r) if err != nil { return nil, err } else if len(r) == 0 { diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 6f4e9a797..1577fbaff 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -2,7 +2,6 @@ package models import ( "code.gitea.io/gitea/modules/timeutil" - "fmt" ) const ( @@ -17,8 +16,13 @@ const ( RewardTypePoint RewardType = "POINT" ) -func (r *RewardType) String() string { - return fmt.Sprint(r) +func (r RewardType) Name() string { + switch r { + case RewardTypePoint: + return "POINT" + default: + return "" + } } const ( diff --git a/models/task_accomplish_log.go b/models/task_accomplish_log.go index 707c214f5..3736d1c41 100644 --- a/models/task_accomplish_log.go +++ b/models/task_accomplish_log.go @@ -11,7 +11,6 @@ type TaskAccomplishLog struct { 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"` } @@ -31,15 +30,7 @@ func getTaskAccomplishLog(tl *TaskAccomplishLog) (*TaskAccomplishLog, error) { return tl, nil } -func GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskCode string) (*TaskAccomplishLog, error) { - t := &TaskAccomplishLog{ - SourceId: sourceId, - TaskCode: taskCode, - } - return getTaskAccomplishLog(t) -} - -func CountInTaskPeriod(configId int64, userId int64, period *PeriodResult) (int64, error) { +func CountTaskAccomplishLogInTaskPeriod(configId int64, userId int64, period *PeriodResult) (int64, error) { if period == nil { return x.Where("config_id = ? and user_id = ?", configId, userId).Count(&TaskAccomplishLog{}) } else { diff --git a/models/task_config.go b/models/task_config.go index aa09ee603..fe2bb7721 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -5,8 +5,24 @@ import ( ) const ( - TaskTypeCreateIssueComment string = "CREATE_IS" - TaskTypeNewIssue = "NEW_ISSUE" + TaskTypeNewIssue = "NEW_ISSUE" + TaskTypeIssueChangeStatus = "ISSUE_CHANGE_STATUS" + TaskTypeCreateIssueComment = "CREATE_ISSUE_COMMENT" + TaskTypeNewPullRequest = "NEW_PULL_REQUEST" + TaskTypeRenameRepository = "RENAME_REPOSITORY" + TaskTypeAliasRepository = "ALIAS_REPOSITORY" + TaskTypeTransferRepository = "TRANSFER_REPOSITORY" + TaskTypeCreateRepository = "CREATE_REPOSITORY" + TaskTypeForkRepository = "FORK_REPOSITORY" + TaskTypePullRequestReview = "PULL_REQUEST_REVIEW" + TaskTypeCommentPull = "COMMENT_PULL" + TaskTypeApprovePullRequest = "APPROVE_PULL_REQUEST" + TaskTypeRejectPullRequest = "REJECT_PULL_REQUEST" + TaskTypeMergePullRequest = "MERGE_PULL_REQUEST" + TaskTypeSyncPushCommits = "SYNC_PUSH_COMMITS" + TaskTypeSyncCreateRef = "SYNC_CREATE_REF" + TaskTypeSyncDeleteRef = "SYNC_DELETE_REF" + TaskTypeBindWechat = "BIND_WECHAT" ) const ( diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index 2ac73c2c3..943678a0b 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -345,3 +345,7 @@ func (a *actionNotifier) NotifyOtherTask(doer *models.User, repo *models.Reposit log.Error("notifyWatchers: %v", err) } } + +func (a *actionNotifier) NotifyWechatBind(doer *models.User) { + return +} diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 8d6fdeb52..26cd1feb8 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -56,4 +56,5 @@ type Notifier interface { NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) + NotifyWechatBind(doer *models.User) } diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 0d3489882..ecdebd7a3 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -158,3 +158,7 @@ func (*NullNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Reposit func (*NullNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) { } + +func (*NullNotifier) NotifyWechatBind(doer *models.User) { + +} diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 8329ca903..2f0335c5a 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -271,3 +271,10 @@ func NotifySyncDeleteRef(pusher *models.User, repo *models.Repository, refType, notifier.NotifySyncDeleteRef(pusher, repo, refType, refFullName) } } + +// NotifyWechatBind notifies wechat bind +func NotifyWechatBind(doer *models.User) { + for _, notifier := range notifiers { + notifier.NotifyWechatBind(doer) + } +} diff --git a/modules/notification/task/task.go b/modules/notification/task/task.go index ce3b023ba..f68872c01 100644 --- a/modules/notification/task/task.go +++ b/modules/notification/task/task.go @@ -5,7 +5,7 @@ import ( "code.gitea.io/gitea/modules/notification/base" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/services/task" - "fmt" + "strings" ) type taskNotifier struct { @@ -22,64 +22,86 @@ func NewNotifier() base.Notifier { } func (t *taskNotifier) NotifyNewIssue(issue *models.Issue) { - task.Accomplish(issue.Poster.ID, models.TaskTypeNewIssue, fmt.Sprint(issue.ID)) + task.Accomplish(issue.Poster.ID, models.TaskTypeNewIssue) } // NotifyIssueChangeStatus notifies close or reopen issue to notifiers func (t *taskNotifier) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) { - return + task.Accomplish(doer.ID, models.TaskTypeIssueChangeStatus) } // NotifyCreateIssueComment notifies comment on an issue to notifiers func (t *taskNotifier) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, issue *models.Issue, comment *models.Comment) { - task.Accomplish(doer.ID, models.TaskTypeCreateIssueComment, fmt.Sprint(comment.ID)) + task.Accomplish(doer.ID, models.TaskTypeCreateIssueComment) } func (t *taskNotifier) NotifyNewPullRequest(pull *models.PullRequest) { - task.Accomplish(pull.Issue.Poster.ID, models.TaskTypeCreateIssueComment, fmt.Sprint(pull.ID)) + task.Accomplish(pull.Issue.Poster.ID, models.TaskTypeNewPullRequest) } func (t *taskNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldRepoName string) { - return + task.Accomplish(doer.ID, models.TaskTypeRenameRepository) } func (t *taskNotifier) NotifyAliasRepository(doer *models.User, repo *models.Repository, oldAlias string) { - return + task.Accomplish(doer.ID, models.TaskTypeAliasRepository) } func (t *taskNotifier) NotifyTransferRepository(doer *models.User, repo *models.Repository, oldOwnerName string) { - return + task.Accomplish(doer.ID, models.TaskTypeTransferRepository) } func (t *taskNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { - return + task.Accomplish(doer.ID, models.TaskTypeCreateRepository) } func (t *taskNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { - return + task.Accomplish(doer.ID, models.TaskTypeForkRepository) } func (t *taskNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment) { - return + for _, lines := range review.CodeComments { + for _, comments := range lines { + for _, _ = range comments { + task.Accomplish(review.Reviewer.ID, models.TaskTypePullRequestReview) + } + } + } + if review.Type != models.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" { + + switch review.Type { + case models.ReviewTypeApprove: + task.Accomplish(review.Reviewer.ID, models.TaskTypeApprovePullRequest) + case models.ReviewTypeReject: + task.Accomplish(review.Reviewer.ID, models.TaskTypeRejectPullRequest) + default: + task.Accomplish(review.Reviewer.ID, models.TaskTypeCommentPull) + } + + } } func (t *taskNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User) { - return + task.Accomplish(doer.ID, models.TaskTypeMergePullRequest) } func (t *taskNotifier) NotifySyncPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) { - return + task.Accomplish(repo.OwnerID, models.TaskTypeSyncPushCommits) } func (t *taskNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) { - return + task.Accomplish(repo.OwnerID, models.TaskTypeSyncCreateRef) } func (t *taskNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) { - return + task.Accomplish(repo.OwnerID, models.TaskTypeSyncDeleteRef) } func (t *taskNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) { return } + +func (t *taskNotifier) NotifyWechatBind(doer *models.User) { + task.Accomplish(doer.ID, models.TaskTypeSyncDeleteRef) +} diff --git a/modules/redis/redis_key/limit_redis_key.go b/modules/redis/redis_key/limit_redis_key.go index 86a77e59e..a58a70fdb 100644 --- a/modules/redis/redis_key/limit_redis_key.go +++ b/modules/redis/redis_key/limit_redis_key.go @@ -7,20 +7,20 @@ import ( const LIMIT_REDIS_PREFIX = "limit" -func LimitCount(userId int64, limitCode string, period *models.PeriodResult) string { - if userId == 0 { +func LimitCount(userId int64, limitCode string, limitType string, scope string, period *models.PeriodResult) string { + if scope == models.LimitScopeAllUsers.Name() { if period == nil { - return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, "count") + return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, limitType, "count") } - return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") + return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, limitType, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") } if period == nil { - return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, "count") + return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, limitType, "count") } - return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") + return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, limitType, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") } -func LimitConfig(limitCode string, limitType models.LimitType) string { - return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, limitType.Name(), "config") +func LimitConfig(limitType models.LimitType) string { + return KeyJoin(LIMIT_REDIS_PREFIX, limitType.Name(), "config") } diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go index 8117ba173..04cef2e2c 100644 --- a/services/reward/limiter/limiter.go +++ b/services/reward/limiter/limiter.go @@ -39,6 +39,7 @@ func (l *limiterRunner) Run() error { err := l.limit(l.limiters[l.index]) if err != nil { log.Info("limiter check failed,%v", err) + l.Rollback(l.index) return err } l.index += 1 @@ -46,20 +47,50 @@ func (l *limiterRunner) Run() error { return nil } +//Rollback rollback the usedNum from limiters[0] to limiters[index] +func (l *limiterRunner) Rollback(index int) error { + for i := index; i >= 0; i-- { + l.rollback(l.limiters[i]) + } + return nil +} + +func (l *limiterRunner) rollback(r models.LimitConfig) error { + p, err := period.GetPeriod(r.RefreshRate) + if err != nil { + return err + } + redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p) + redis_client.IncrBy(redisKey, -1*l.amount) + return nil +} + func (l *limiterRunner) limit(r models.LimitConfig) error { p, err := period.GetPeriod(r.RefreshRate) if err != nil { return err } - redisKey := redis_key.LimitCount(l.userId, r.LimitCode, p) + redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p) usedNum, err := redis_client.IncrBy(redisKey, l.amount) - //if it is the first time,set expire time - if usedNum == l.amount && p != nil { - //todo 验证浮点精确度 - redis_client.Expire(redisKey, int64(p.LeftTime.Seconds())) + if err != nil { + return err + } + //if usedNum equals amount,it is the first operation in period or redis cache deleted + //count in database to distinguish the two cases + if usedNum == l.amount { + n, err := l.countInPeriod(r, p) + if err != nil { + return err + } + if n > 0 { + //means redis cache deleted,incr the cache with real value + usedNum, err = redis_client.IncrBy(redisKey, n) + } + if p != nil { + redis_client.Expire(redisKey, int64(p.LeftTime.Seconds())) + } } if usedNum > r.LimitNum { - redis_client.IncrBy(redisKey, -1*l.amount) return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) } return nil @@ -76,13 +107,37 @@ func (l *limiterRunner) LoadLimiters() error { return nil } +func (l *limiterRunner) countInPeriod(r models.LimitConfig, p *models.PeriodResult) (int64, error) { + switch r.LimitType { + case models.LimitTypeTask.Name(): + return models.CountTaskAccomplishLogInTaskPeriod(r.ID, l.userId, p) + default: + return 0, nil + + } +} + func CheckLimit(limitCode string, limitType models.LimitType, userId, amount int64) error { r := newLimiterRunner(limitCode, limitType, userId, amount) return r.Run() } func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitConfig, error) { - redisKey := redis_key.LimitConfig(limitCode, limitType) + limiters, err := GetLimitersByLimitType(limitType) + if err != nil { + return nil, err + } + result := make([]models.LimitConfig, 0) + for i, v := range limiters { + if v.LimitCode == "" || v.LimitCode == limitCode { + result = append(result, limiters[i]) + } + } + return result, nil +} + +func GetLimitersByLimitType(limitType models.LimitType) ([]models.LimitConfig, error) { + redisKey := redis_key.LimitConfig(limitType) val, _ := redis_client.Get(redisKey) if val != "" { if val == redis_key.EMPTY_REDIS_VAL { @@ -92,7 +147,7 @@ func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitCo json.Unmarshal([]byte(val), &limiters) return limiters, nil } - limiters, err := models.GetLimitConfigByLimitCode(limitCode, limitType) + limiters, err := models.GetLimitConfigByLimitType(limitType) if err != nil { if models.IsErrRecordNotExist(err) { redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) diff --git a/services/reward/operator.go b/services/reward/operator.go index b0bd53f8a..8d24ed055 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -58,7 +58,7 @@ func Send(ctx models.RewardOperateContext) error { } //is limited? - if operator.IsLimited(ctx) { + if isLimited := operator.IsLimited(ctx); isLimited { return nil } diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index eeba83ac7..80b0b4fe9 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -15,7 +15,7 @@ type PointOperator struct { } func (operator *PointOperator) IsLimited(ctx models.RewardOperateContext) bool { - if err := limiter.CheckLimit(ctx.Reward.Type, models.LimitTypeReward, ctx.TargetUserId, ctx.Reward.Amount); err != nil { + if err := limiter.CheckLimit(ctx.SourceType, models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount); err != nil { return true } return false diff --git a/services/task/task.go b/services/task/task.go index f38793419..737094b4e 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -3,49 +3,23 @@ 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/modules/util" "code.gitea.io/gitea/services/reward" "code.gitea.io/gitea/services/reward/limiter" "fmt" - "time" ) -func Accomplish(userId int64, taskType string, sourceId string) { - go accomplish(userId, taskType, sourceId) +func Accomplish(userId int64, taskType string) { + go accomplish(userId, taskType) } -func accomplish(userId int64, taskType string, sourceId string) error { +func accomplish(userId int64, taskType string) error { defer func() { if err := recover(); err != nil { combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) log.Error("PANIC:%v", combinedErr) } }() - //lock - var taskLock = redis_lock.NewDistributeLock(redis_key.TaskAccomplishLock(sourceId, taskType)) - isOk, err := taskLock.Lock(3 * time.Second) - if err != nil { - log.Error("get taskLock error. %v", err) - return err - } - if !isOk { - 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) @@ -54,13 +28,13 @@ func accomplish(userId int64, taskType string, sourceId string) error { return err } if config == nil { - log.Info("task config not exist,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) + log.Info("task config not exist,userId=%d taskType=%s", userId, taskType) return nil } //is limited? if isLimited(userId, config) { - log.Info("task accomplish maximum times are reached,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) + log.Info("task accomplish maximum times are reached,userId=%d taskType=%s", userId, taskType) return nil } @@ -71,7 +45,6 @@ func accomplish(userId int64, taskType string, sourceId string) error { ConfigId: config.ID, TaskCode: config.TaskCode, UserId: userId, - SourceId: sourceId, }) if err != nil { return err @@ -93,18 +66,6 @@ func accomplish(userId int64, taskType string, sourceId string) error { return nil } -func isHandled(taskType string, sourceId string) (bool, error) { - _, err := models.GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskType) - if err != nil { - if models.IsErrRecordNotExist(err) { - return false, nil - } - return false, err - } - return true, nil - -} - func isLimited(userId int64, config *models.TaskConfig) bool { if err := limiter.CheckLimit(config.TaskCode, models.LimitTypeTask, userId, 1); err != nil { return true From 0562bee1c2fc68f34009577bb500cd3f5c758571 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 8 Jun 2022 15:07:44 +0800 Subject: [PATCH 007/283] #1249 update task trigger --- models/attachment.go | 8 ++ models/reward_operate_record.go | 19 +++- models/task_config.go | 45 ++++---- modules/auth/wechat/event_handle.go | 3 +- modules/cloudbrain/resty.go | 17 +-- modules/notification/action/action.go | 4 - modules/notification/base/notifier.go | 6 +- modules/notification/base/null.go | 14 ++- modules/notification/notification.go | 32 +++++- modules/notification/task/task.go | 56 +++++++++- routers/admin/dataset.go | 3 + routers/image/image.go | 2 + routers/repo/ai_model_manage.go | 1 - routers/repo/cloudbrain.go | 1 - routers/user/setting/profile.go | 2 + services/reward/limiter/limiter.go | 139 +++++++++++++++++++++---- services/reward/point/point_operate.go | 10 +- 17 files changed, 300 insertions(+), 62 deletions(-) diff --git a/models/attachment.go b/models/attachment.go index ea8f1645f..0e4751ed2 100755 --- a/models/attachment.go +++ b/models/attachment.go @@ -653,3 +653,11 @@ func Attachments(opts *AttachmentsOptions) ([]*AttachmentInfo, int64, error) { return attachments, count, nil } + +func GetAllUserIdByDatasetId(datasetId int64) ([]int64, error) { + r := make([]int64, 0) + if err := x.Table("attachment").Where("dataset_id = ?", datasetId).Distinct("uploader_id").Find(&r); err != nil { + return nil, err + } + return r, nil +} diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 1577fbaff..b1b9983c3 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -2,6 +2,7 @@ package models import ( "code.gitea.io/gitea/modules/timeutil" + "xorm.io/builder" ) const ( @@ -26,8 +27,8 @@ func (r RewardType) Name() string { } const ( - OperateTypeIncrease = "INCREASE_POINT" - OperateTypeDecrease = "DECREASE_POINT" + OperateTypeIncrease = "INCREASE" + OperateTypeDecrease = "DECREASE" ) const ( @@ -82,6 +83,20 @@ func UpdateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus return x.Cols("status").Where("source_type=? and request_id=? and status=?", sourceType, requestId, oldStatus).Update(r) } +func SumRewardAmountInTaskPeriod(rewardType string, sourceType string, userId int64, period *PeriodResult) (int64, error) { + var cond = builder.NewCond() + if period != nil { + cond = cond.And(builder.Gte{"created_unix": period.StartTime.Unix()}) + cond = cond.And(builder.Lt{"created_unix": period.EndTime.Unix()}) + } + if sourceType != "" { + cond = cond.And(builder.Eq{"source_type": sourceType}) + } + cond = cond.And(builder.Eq{"reward_type": rewardType}) + cond = cond.And(builder.Eq{"user_id": userId}) + return x.Where(cond).SumInt(&RewardOperateRecord{}, "amount") +} + type RewardOperateContext struct { SourceType string SourceId string diff --git a/models/task_config.go b/models/task_config.go index fe2bb7721..c9e352ed0 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -5,24 +5,33 @@ import ( ) const ( - TaskTypeNewIssue = "NEW_ISSUE" - TaskTypeIssueChangeStatus = "ISSUE_CHANGE_STATUS" - TaskTypeCreateIssueComment = "CREATE_ISSUE_COMMENT" - TaskTypeNewPullRequest = "NEW_PULL_REQUEST" - TaskTypeRenameRepository = "RENAME_REPOSITORY" - TaskTypeAliasRepository = "ALIAS_REPOSITORY" - TaskTypeTransferRepository = "TRANSFER_REPOSITORY" - TaskTypeCreateRepository = "CREATE_REPOSITORY" - TaskTypeForkRepository = "FORK_REPOSITORY" - TaskTypePullRequestReview = "PULL_REQUEST_REVIEW" - TaskTypeCommentPull = "COMMENT_PULL" - TaskTypeApprovePullRequest = "APPROVE_PULL_REQUEST" - TaskTypeRejectPullRequest = "REJECT_PULL_REQUEST" - TaskTypeMergePullRequest = "MERGE_PULL_REQUEST" - TaskTypeSyncPushCommits = "SYNC_PUSH_COMMITS" - TaskTypeSyncCreateRef = "SYNC_CREATE_REF" - TaskTypeSyncDeleteRef = "SYNC_DELETE_REF" - TaskTypeBindWechat = "BIND_WECHAT" + TaskTypeNewIssue = "NEW_ISSUE" + TaskTypeIssueChangeStatus = "ISSUE_CHANGE_STATUS" + TaskTypeCreateIssueComment = "CREATE_ISSUE_COMMENT" + TaskTypeNewPullRequest = "NEW_PULL_REQUEST" + TaskTypeRenameRepository = "RENAME_REPOSITORY" + TaskTypeAliasRepository = "ALIAS_REPOSITORY" + TaskTypeTransferRepository = "TRANSFER_REPOSITORY" + TaskTypeCreateRepository = "CREATE_REPOSITORY" + TaskTypeCreatePublicRepository = "CREATE_PUBLIC_REPOSITORY" + TaskTypeForkRepository = "FORK_REPOSITORY" + TaskTypePullRequestReview = "PULL_REQUEST_REVIEW" + TaskTypeCommentPull = "COMMENT_PULL" + TaskTypeApprovePullRequest = "APPROVE_PULL_REQUEST" + TaskTypeRejectPullRequest = "REJECT_PULL_REQUEST" + TaskTypeMergePullRequest = "MERGE_PULL_REQUEST" + TaskTypeSyncPushCommits = "SYNC_PUSH_COMMITS" + TaskTypeSyncCreateRef = "SYNC_CREATE_REF" + TaskTypeSyncDeleteRef = "SYNC_DELETE_REF" + TaskTypeBindWechat = "BIND_WECHAT" + TaskTypeUploadAttachment = "UPLOAD_ATTACHMENT" + TaskTypeCreateCloudbrainTask = "CREATE_CLOUDBRAIN_TASK" + TaskTypeDatasetRecommended = "DATASET_RECOMMENDED" + TaskTypeCreateModel = "CREATE_MODEL" + TaskTypeCreatePublicImage = "CREATE_PUBLIC_IMAGE" + TaskTypeImageRecommend = "IMAGE_RECOMMEND" + TaskTypeChangeUserAvatar = "CHANGE_USER_AVATAR" + TaskTypePushCommits = "PUSH_COMMITS" ) const ( diff --git a/modules/auth/wechat/event_handle.go b/modules/auth/wechat/event_handle.go index b40ab3101..67c3a7265 100644 --- a/modules/auth/wechat/event_handle.go +++ b/modules/auth/wechat/event_handle.go @@ -1,6 +1,7 @@ package wechat import ( + "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "encoding/json" @@ -71,6 +72,6 @@ func HandleSubscribeEvent(we WechatEvent) string { jsonStr, _ := json.Marshal(qrCache) redis_client.Setex(redis_key.WechatBindingUserIdKey(sceneStr), string(jsonStr), 60*time.Second) } - + notification.NotifyWechatBind(qrCache.UserId, we.FromUserName) return BIND_REPLY_SUCCESS } diff --git a/modules/cloudbrain/resty.go b/modules/cloudbrain/resty.go index e70dbdd2b..75614e571 100755 --- a/modules/cloudbrain/resty.go +++ b/modules/cloudbrain/resty.go @@ -1,6 +1,7 @@ package cloudbrain import ( + "code.gitea.io/gitea/modules/notification" "encoding/json" "errors" "fmt" @@ -24,10 +25,10 @@ var ( ) const ( - JobHasBeenStopped = "S410" - Public = "public" - Custom = "custom" - LogPageSize = 500 + JobHasBeenStopped = "S410" + Public = "public" + Custom = "custom" + LogPageSize = 500 LogPageTokenExpired = "5m" pageSize = 15 ) @@ -313,6 +314,7 @@ sendjob: }) if err == nil { go updateImageStatus(image, isSetCreatedUnix, createTime) + notification.NotifyCreateImage(params.UID, image) } return err } @@ -354,6 +356,9 @@ func CommitAdminImage(params models.CommitImageParams) error { } return nil }) + if err == nil { + notification.NotifyCreateImage(params.UID, image) + } return err } @@ -474,7 +479,7 @@ func GetJobAllLog(scrollID string) (*models.GetJobLogResult, error) { client := getRestyClient() var result models.GetJobLogResult req := models.GetAllJobLogParams{ - Scroll: LogPageTokenExpired, + Scroll: LogPageTokenExpired, ScrollID: scrollID, } @@ -498,7 +503,7 @@ func GetJobAllLog(scrollID string) (*models.GetJobLogResult, error) { return &result, nil } -func DeleteJobLogToken(scrollID string) (error) { +func DeleteJobLogToken(scrollID string) error { checkSetting() client := getRestyClient() var result models.DeleteJobLogTokenResult diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index 943678a0b..2ac73c2c3 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -345,7 +345,3 @@ func (a *actionNotifier) NotifyOtherTask(doer *models.User, repo *models.Reposit log.Error("notifyWatchers: %v", err) } } - -func (a *actionNotifier) NotifyWechatBind(doer *models.User) { - return -} diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 26cd1feb8..c3c7f404a 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -56,5 +56,9 @@ type Notifier interface { NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) - NotifyWechatBind(doer *models.User) + NotifyWechatBind(userId int64, wechatOpenId string) + NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) + NotifyCreateImage(optUserId int64, image models.Image) + NotifyImageRecommend(optUser *models.User, imageId int64, action string) + NotifyChangeUserAvatar(user *models.User) } diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index ecdebd7a3..c0a224697 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -159,6 +159,18 @@ func (*NullNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, } -func (*NullNotifier) NotifyWechatBind(doer *models.User) { +func (*NullNotifier) NotifyWechatBind(userId int64, wechatOpenId string) { } + +func (*NullNotifier) NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) { +} + +func (*NullNotifier) NotifyCreateImage(optUserId int64, image models.Image) { +} + +func (*NullNotifier) NotifyImageRecommend(optUser *models.User, imageId int64, action string) { +} + +func (*NullNotifier) NotifyChangeUserAvatar(user *models.User) { +} diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 2f0335c5a..118bdf994 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -273,8 +273,36 @@ func NotifySyncDeleteRef(pusher *models.User, repo *models.Repository, refType, } // NotifyWechatBind notifies wechat bind -func NotifyWechatBind(doer *models.User) { +func NotifyWechatBind(userId int64, wechatOpenId string) { for _, notifier := range notifiers { - notifier.NotifyWechatBind(doer) + notifier.NotifyWechatBind(userId, wechatOpenId) + } +} + +// NotifyDatasetRecommend +func NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) { + for _, notifier := range notifiers { + notifier.NotifyDatasetRecommend(optUser, dataset, action) + } +} + +// NotifyDatasetRecommend +func NotifyCreateImage(optUserId int64, image models.Image) { + for _, notifier := range notifiers { + notifier.NotifyCreateImage(optUserId, image) + } +} + +// NotifyDatasetRecommend +func NotifyImageRecommend(optUser *models.User, imageId int64, action string) { + for _, notifier := range notifiers { + notifier.NotifyImageRecommend(optUser, imageId, action) + } +} + +// NotifyDatasetRecommend +func NotifyChangeUserAvatar(user *models.User) { + for _, notifier := range notifiers { + notifier.NotifyChangeUserAvatar(user) } } diff --git a/modules/notification/task/task.go b/modules/notification/task/task.go index f68872c01..077d6699b 100644 --- a/modules/notification/task/task.go +++ b/modules/notification/task/task.go @@ -53,7 +53,10 @@ func (t *taskNotifier) NotifyTransferRepository(doer *models.User, repo *models. } func (t *taskNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { - task.Accomplish(doer.ID, models.TaskTypeCreateRepository) + if !repo.IsPrivate { + task.Accomplish(doer.ID, models.TaskTypeCreatePublicRepository) + } + } func (t *taskNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { @@ -99,9 +102,56 @@ func (t *taskNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repos } func (t *taskNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) { + switch optype { + case models.ActionUploadAttachment: + task.Accomplish(doer.ID, models.TaskTypeUploadAttachment) + case models.ActionCreateDebugGPUTask, + models.ActionCreateDebugNPUTask, + models.ActionCreateTrainTask, + models.ActionCreateInferenceTask, + models.ActionCreateBenchMarkTask, + models.ActionCreateGPUTrainTask: + task.Accomplish(doer.ID, models.TaskTypeCreateCloudbrainTask) + case models.ActionCreateNewModelTask: + task.Accomplish(doer.ID, models.TaskTypeCreateModel) + } return } -func (t *taskNotifier) NotifyWechatBind(doer *models.User) { - task.Accomplish(doer.ID, models.TaskTypeSyncDeleteRef) +func (t *taskNotifier) NotifyWechatBind(userId int64, wechatOpenId string) { + task.Accomplish(userId, models.TaskTypeBindWechat) +} + +func (t *taskNotifier) NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) { + switch action { + case "recommend": + userIds, err := models.GetAllUserIdByDatasetId(dataset.ID) + if err != nil { + return + } + for _, userId := range userIds { + task.Accomplish(userId, models.TaskTypeDatasetRecommended) + } + } +} + +func (t *taskNotifier) NotifyCreateImage(optUserId int64, image models.Image) { + if !image.IsPrivate { + task.Accomplish(optUserId, models.TaskTypeCreatePublicImage) + } +} + +func (t *taskNotifier) NotifyImageRecommend(optUser *models.User, imageId int64, action string) { + switch action { + case "recommend": + task.Accomplish(optUser.ID, models.TaskTypeImageRecommend) + } +} + +func (t *taskNotifier) NotifyChangeUserAvatar(user *models.User) { + task.Accomplish(user.ID, models.TaskTypeChangeUserAvatar) +} + +func (t *taskNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) { + task.Accomplish(pusher.ID, models.TaskTypePushCommits) } diff --git a/routers/admin/dataset.go b/routers/admin/dataset.go index 6b29b06ff..0eb5d27ab 100644 --- a/routers/admin/dataset.go +++ b/routers/admin/dataset.go @@ -1,6 +1,7 @@ package admin import ( + "code.gitea.io/gitea/modules/notification" "net/http" "strconv" "strings" @@ -106,6 +107,8 @@ func DatasetAction(ctx *context.Context) { if err != nil { ctx.JSON(http.StatusOK, models.BaseErrorMessage(ctx.Tr("repo.star_fail", ctx.Params(":action")))) } else { + d, _ := models.GetDatasetByID(datasetId) + notification.NotifyDatasetRecommend(ctx.User, d, ctx.Params(":action")) ctx.JSON(http.StatusOK, models.BaseOKMessage) } } diff --git a/routers/image/image.go b/routers/image/image.go index ae9912e3d..e238387ab 100644 --- a/routers/image/image.go +++ b/routers/image/image.go @@ -1,6 +1,7 @@ package image import ( + "code.gitea.io/gitea/modules/notification" "net/http" "strconv" @@ -25,6 +26,7 @@ func Action(ctx *context.Context) { if err != nil { ctx.JSON(http.StatusOK, models.BaseErrorMessage(ctx.Tr("repo.star_fail", ctx.Params(":action")))) } else { + notification.NotifyImageRecommend(ctx.User, imageId, ctx.Params(":action")) ctx.JSON(http.StatusOK, models.BaseOKMessage) } } diff --git a/routers/repo/ai_model_manage.go b/routers/repo/ai_model_manage.go index e2040e0d2..3ad37f1f6 100644 --- a/routers/repo/ai_model_manage.go +++ b/routers/repo/ai_model_manage.go @@ -170,7 +170,6 @@ func SaveModel(ctx *context.Context) { ctx.Error(500, fmt.Sprintf("save model error. %v", err)) return } - log.Info("save model end.") } diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index a5dd52956..7ed6fa6ef 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -783,7 +783,6 @@ func CloudBrainCommitImage(ctx *context.Context, form auth.CommitImageCloudBrain return } - ctx.JSON(200, models.BaseOKMessage) } diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go index 3333a8cc4..1c1e664d0 100755 --- a/routers/user/setting/profile.go +++ b/routers/user/setting/profile.go @@ -6,6 +6,7 @@ package setting import ( + "code.gitea.io/gitea/modules/notification" "errors" "fmt" "io/ioutil" @@ -165,6 +166,7 @@ func AvatarPost(ctx *context.Context, form auth.AvatarForm) { if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil { ctx.Flash.Error(err.Error()) } else { + notification.NotifyChangeUserAvatar(ctx.User) ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) } diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go index 04cef2e2c..fafaab9cb 100644 --- a/services/reward/limiter/limiter.go +++ b/services/reward/limiter/limiter.go @@ -12,56 +12,118 @@ import ( "time" ) +type limiterRejectPolicy string + +const ( + JustReject limiterRejectPolicy = "JUST_REJECT" + PermittedOnce limiterRejectPolicy = "PERMITTED_ONCE" + FillUp limiterRejectPolicy = "FillUp" +) + type limiterRunner struct { - limiters []models.LimitConfig - index int - userId int64 - amount int64 - limitCode string - limitType models.LimitType + limiters []models.LimitConfig + index int + userId int64 + amount int64 + limitCode string + limitType models.LimitType + rejectPolicy limiterRejectPolicy + resultMap map[int]limitResult + minRealAmount int64 +} + +type limitResult struct { + isLoss bool + planAmount int64 + realAmount int64 +} + +func newLimitResult(isLoss bool, planAmount int64, realAmount int64) limitResult { + return limitResult{ + isLoss: isLoss, + planAmount: planAmount, + realAmount: realAmount, + } } -func newLimiterRunner(limitCode string, limitType models.LimitType, userId, amount int64) *limiterRunner { +func newLimiterRunner(limitCode string, limitType models.LimitType, userId, amount int64, policy limiterRejectPolicy) *limiterRunner { return &limiterRunner{ - userId: userId, - amount: amount, - limitCode: limitCode, - limitType: limitType, - index: 0, + userId: userId, + amount: amount, + limitCode: limitCode, + limitType: limitType, + index: 0, + rejectPolicy: policy, + resultMap: make(map[int]limitResult, 0), } } +//Run run all limiters +//return real used amount(when choose the FillUp reject policy, amount may only be partially used) func (l *limiterRunner) Run() error { if err := l.LoadLimiters(); err != nil { return err } + l.minRealAmount = l.amount for l.index < len(l.limiters) { err := l.limit(l.limiters[l.index]) if err != nil { log.Info("limiter check failed,%v", err) - l.Rollback(l.index) + l.Rollback() return err } + result := l.resultMap[l.index] + if result.isLoss { + //find the minimum real amount + if l.minRealAmount > result.realAmount { + l.minRealAmount = result.realAmount + } + } l.index += 1 } + + //post process + l.PostProcess() return nil } //Rollback rollback the usedNum from limiters[0] to limiters[index] -func (l *limiterRunner) Rollback(index int) error { - for i := index; i >= 0; i-- { - l.rollback(l.limiters[i]) +func (l *limiterRunner) Rollback() error { + for i := l.index - 1; i >= 0; i-- { + l.rollback(l.limiters[i], l.resultMap[i]) + } + return nil +} + +func (l *limiterRunner) rollback(r models.LimitConfig, result limitResult) error { + p, err := period.GetPeriod(r.RefreshRate) + if err != nil { + return err + } + redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p) + redis_client.IncrBy(redisKey, -1*result.realAmount) + return nil +} + +//PostProcess process loss,if realAmount < planAmount +func (l *limiterRunner) PostProcess() error { + for i := l.index - 1; i >= 0; i-- { + l.postProcess(l.limiters[i], l.resultMap[i]) } return nil } -func (l *limiterRunner) rollback(r models.LimitConfig) error { +func (l *limiterRunner) postProcess(r models.LimitConfig, result limitResult) error { + if result.realAmount == l.minRealAmount { + return nil + } p, err := period.GetPeriod(r.RefreshRate) if err != nil { return err } + diff := result.realAmount - l.minRealAmount redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p) - redis_client.IncrBy(redisKey, -1*l.amount) + redis_client.IncrBy(redisKey, -1*diff) return nil } @@ -91,8 +153,25 @@ func (l *limiterRunner) limit(r models.LimitConfig) error { } } if usedNum > r.LimitNum { - return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) + if usedNum-r.LimitNum >= l.amount { + redis_client.IncrBy(redisKey, -1*l.amount) + return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) + } + switch l.rejectPolicy { + case FillUp: + exceed := usedNum - r.LimitNum + realAmount := l.amount - exceed + redis_client.IncrBy(redisKey, -1*exceed) + l.resultMap[l.index] = newLimitResult(true, l.amount, realAmount) + case JustReject: + redis_client.IncrBy(redisKey, -1*l.amount) + return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) + case PermittedOnce: + l.resultMap[l.index] = newLimitResult(false, l.amount, l.amount) + } + } + l.resultMap[l.index] = newLimitResult(false, l.amount, l.amount) return nil } @@ -111,15 +190,33 @@ func (l *limiterRunner) countInPeriod(r models.LimitConfig, p *models.PeriodResu switch r.LimitType { case models.LimitTypeTask.Name(): return models.CountTaskAccomplishLogInTaskPeriod(r.ID, l.userId, p) + case models.LimitTypeRewardPoint.Name(): + return models.SumRewardAmountInTaskPeriod(models.RewardTypePoint.Name(), r.LimitCode, l.userId, p) default: return 0, nil } } +func CheckLimitWithFillUp(limitCode string, limitType models.LimitType, userId, amount int64) (int64, error) { + r := newLimiterRunner(limitCode, limitType, userId, amount, FillUp) + err := r.Run() + if err != nil { + return 0, err + } + return r.minRealAmount, nil +} + +func CheckLimitWithPermittedOnce(limitCode string, limitType models.LimitType, userId, amount int64) error { + r := newLimiterRunner(limitCode, limitType, userId, amount, PermittedOnce) + err := r.Run() + return err +} + func CheckLimit(limitCode string, limitType models.LimitType, userId, amount int64) error { - r := newLimiterRunner(limitCode, limitType, userId, amount) - return r.Run() + r := newLimiterRunner(limitCode, limitType, userId, amount, JustReject) + err := r.Run() + return err } func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitConfig, error) { diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 80b0b4fe9..eaebdf764 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -8,16 +8,24 @@ import ( "code.gitea.io/gitea/services/reward/limiter" "code.gitea.io/gitea/services/reward/point/account" "errors" + "fmt" "time" ) +const LossMsg = "达到奖励上限,应得%d积分,实得%d积分" + type PointOperator struct { } func (operator *PointOperator) IsLimited(ctx models.RewardOperateContext) bool { - if err := limiter.CheckLimit(ctx.SourceType, models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount); err != nil { + realAmount, err := limiter.CheckLimitWithFillUp(ctx.SourceType, models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount) + if err != nil { return true } + if realAmount < ctx.Reward.Amount { + ctx.Remark = ctx.Remark + ";" + fmt.Sprintf(LossMsg, ctx.Reward.Amount, realAmount) + ctx.Reward.Amount = realAmount + } return false } From f606783d568b7ec8d205468af757c46df1aaaa1d Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 8 Jun 2022 16:42:06 +0800 Subject: [PATCH 008/283] #1249 add task config query api add point account query api --- models/limit_config.go | 13 ++++- models/task_config.go | 28 ++++++++-- modules/redis/redis_key/task_redis_key.go | 7 ++- routers/response/response.go | 2 +- routers/reward/point/point.go | 31 +++++++++++ routers/routes/routes.go | 10 ++++ routers/task/config.go | 17 ++++++ services/task/task_config.go | 64 +++++++++++++++++++++-- 8 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 routers/reward/point/point.go create mode 100644 routers/task/config.go diff --git a/models/limit_config.go b/models/limit_config.go index aec26a036..75dccac9a 100644 --- a/models/limit_config.go +++ b/models/limit_config.go @@ -45,12 +45,21 @@ type LimitConfig struct { Scope string `xorm:"NOT NULL"` LimitNum int64 `xorm:"NOT NULL"` LimitCode string - LimitType string `xorm:"NOT NULL"` - Creator int64 `xorm:"NOT NULL"` + LimitType string `xorm:"NOT NULL"` + CreatorId int64 `xorm:"NOT NULL"` + CreatorName string CreatedUnix timeutil.TimeStamp `xorm:"created"` DeletedAt timeutil.TimeStamp `xorm:"deleted"` } +type LimitConfigVO struct { + RefreshRate string + Scope string + LimitNum int64 + Creator string + CreatedUnix timeutil.TimeStamp +} + func GetLimitConfigByLimitType(limitType LimitType) ([]LimitConfig, error) { r := make([]LimitConfig, 0) err := x.Where(" limit_type = ?", limitType.Name()).Find(&r) diff --git a/models/task_config.go b/models/task_config.go index c9e352ed0..eee5caea5 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -45,13 +45,24 @@ type TaskConfig struct { ID int64 `xorm:"pk autoincr"` TaskCode string `xorm:"NOT NULL"` Tittle string - AwardType string `xorm:"NOT NULL"` - AwardAmount int64 `xorm:"NOT NULL"` - Creator int64 `xorm:"NOT NULL"` + AwardType string `xorm:"NOT NULL"` + AwardAmount int64 `xorm:"NOT NULL"` + CreatorId int64 `xorm:"NOT NULL"` + CreatorName string CreatedUnix timeutil.TimeStamp `xorm:"created"` DeletedAt timeutil.TimeStamp `xorm:"deleted"` } +type TaskConfigWithLimit struct { + TaskCode string + Tittle string + AwardType string + AwardAmount int64 + Creator string + CreatedUnix timeutil.TimeStamp + Limiters []LimitConfigVO +} + func getTaskConfig(t *TaskConfig) (*TaskConfig, error) { has, err := x.Get(t) if err != nil { @@ -68,3 +79,14 @@ func GetTaskConfigByTaskCode(taskCode string) (*TaskConfig, error) { } return getTaskConfig(t) } +func GetTaskConfigList() ([]*TaskConfig, error) { + r := make([]*TaskConfig, 0) + err := x.Find(&r) + if err != nil { + return nil, err + } + if len(r) == 0 { + return nil, ErrRecordNotExist{} + } + return r, nil +} diff --git a/modules/redis/redis_key/task_redis_key.go b/modules/redis/redis_key/task_redis_key.go index 3427c8f7f..8d6fb3f6e 100644 --- a/modules/redis/redis_key/task_redis_key.go +++ b/modules/redis/redis_key/task_redis_key.go @@ -6,6 +6,9 @@ func TaskAccomplishLock(sourceId string, taskType string) string { return KeyJoin(TASK_REDIS_PREFIX, sourceId, taskType, "accomplish") } -func TaskConfig(taskType string) string { - return KeyJoin(TASK_REDIS_PREFIX, "config", taskType) +func TaskConfigList() string { + return KeyJoin(TASK_REDIS_PREFIX, "config", "list") +} +func TaskConfigWithLimiterList() string { + return KeyJoin(TASK_REDIS_PREFIX, "config", "limiter", "list") } diff --git a/routers/response/response.go b/routers/response/response.go index edd3b9cca..e87471d4c 100644 --- a/routers/response/response.go +++ b/routers/response/response.go @@ -25,7 +25,7 @@ func ServerError(msg string) *AiforgeResponse { } func SuccessWithData(data interface{}) *AiforgeResponse { - return &AiforgeResponse{Code: RESPONSE_CODE_ERROR_DEFAULT, Msg: RESPONSE_MSG_SUCCESS, Data: data} + return &AiforgeResponse{Code: RESPONSE_CODE_SUCCESS, Msg: RESPONSE_MSG_SUCCESS, Data: data} } func ErrorWithData(code int, msg string, data interface{}) *AiforgeResponse { return &AiforgeResponse{Code: code, Msg: msg, Data: data} diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go new file mode 100644 index 000000000..eaae76c4f --- /dev/null +++ b/routers/reward/point/point.go @@ -0,0 +1,31 @@ +package point + +import ( + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/routers/response" + "code.gitea.io/gitea/services/reward/point/account" + "net/http" +) + +type AccountResponse struct { + AccountCode string + Balance int64 + TotalEarned int64 + TotalConsumed int64 +} + +func GetPointAccount(ctx *context.Context) { + userId := ctx.User.ID + a, err := account.GetAccount(userId) + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + res := &AccountResponse{ + AccountCode: a.AccountCode, + Balance: a.Balance, + TotalEarned: a.TotalEarned, + TotalConsumed: a.TotalConsumed, + } + ctx.JSON(http.StatusOK, response.SuccessWithData(res)) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 4c3f5f472..b2393246c 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -6,6 +6,8 @@ package routes import ( "bytes" + "code.gitea.io/gitea/routers/reward/point" + "code.gitea.io/gitea/routers/task" "encoding/gob" "net/http" "path" @@ -1314,6 +1316,14 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/purge", user.NotificationPurgePost) }, reqSignIn) + m.Group("/reward/point", func() { + m.Get("/account", point.GetPointAccount) + }, reqSignIn) + + m.Group("/task/config", func() { + m.Get("/list", task.GetTaskConfigList) + }, reqSignIn) + if setting.API.EnableSwagger { m.Get("/swagger.v1.json", templates.JSONRenderer(), routers.SwaggerV1Json) } diff --git a/routers/task/config.go b/routers/task/config.go new file mode 100644 index 000000000..95db0b7d8 --- /dev/null +++ b/routers/task/config.go @@ -0,0 +1,17 @@ +package task + +import ( + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/routers/response" + "code.gitea.io/gitea/services/task" + "net/http" +) + +func GetTaskConfigList(ctx *context.Context) { + r, err := task.GetTaskConfigWithLimitList() + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + ctx.JSON(http.StatusOK, response.SuccessWithData(r)) +} diff --git a/services/task/task_config.go b/services/task/task_config.go index 6e7f22e14..3f2225d2c 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -4,6 +4,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/services/reward/limiter" "encoding/json" "time" ) @@ -11,17 +12,30 @@ import ( //GetTaskConfig get task config from redis cache first // if not exist in redis, find in db and refresh the redis key func GetTaskConfig(taskType string) (*models.TaskConfig, error) { - redisKey := redis_key.TaskConfig(taskType) + list, err := GetTaskConfigList() + if err != nil { + return nil, err + } + for _, v := range list { + if v.TaskCode == taskType { + return v, nil + } + } + return nil, nil +} + +func GetTaskConfigList() ([]*models.TaskConfig, error) { + redisKey := redis_key.TaskConfigList() configStr, _ := redis_client.Get(redisKey) if configStr != "" { if configStr == redis_key.EMPTY_REDIS_VAL { return nil, nil } - config := new(models.TaskConfig) - json.Unmarshal([]byte(configStr), config) + config := make([]*models.TaskConfig, 0) + json.Unmarshal([]byte(configStr), &config) return config, nil } - config, err := models.GetTaskConfigByTaskCode(taskType) + config, err := models.GetTaskConfigList() if err != nil { if models.IsErrRecordNotExist(err) { redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) @@ -33,3 +47,45 @@ func GetTaskConfig(taskType string) (*models.TaskConfig, error) { redis_client.Setex(redisKey, string(jsonStr), 30*24*time.Hour) return config, nil } + +func GetTaskConfigWithLimitList() ([]*models.TaskConfigWithLimit, error) { + list, err := GetTaskConfigList() + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + r := make([]*models.TaskConfigWithLimit, 0) + l, err := limiter.GetLimitersByLimitType(models.LimitTypeTask) + if err != nil { + return nil, err + } + for i := 0; i < len(list); i++ { + li := list[i] + t := &models.TaskConfigWithLimit{ + TaskCode: li.TaskCode, + Tittle: li.Tittle, + AwardType: li.AwardType, + AwardAmount: li.AwardAmount, + Creator: li.CreatorName, + CreatedUnix: li.CreatedUnix, + } + lv := make([]models.LimitConfigVO, 0) + for j := 0; j < len(l); j++ { + lj := l[j] + if lj.LimitCode == li.TaskCode { + lv = append(lv, models.LimitConfigVO{ + RefreshRate: lj.RefreshRate, + Scope: lj.Scope, + LimitNum: lj.LimitNum, + Creator: lj.CreatorName, + CreatedUnix: lj.CreatedUnix, + }) + } + } + t.Limiters = lv + r = append(r, t) + } + return r, nil +} From 40f7620a3413daa2cb6a030685643078eb71effb Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 9 Jun 2022 16:58:03 +0800 Subject: [PATCH 009/283] #1249 add task config edit api --- models/limit_config.go | 1 + models/task_config.go | 69 ++++++++++++++++++++++- modules/redis/redis_key/task_redis_key.go | 3 - routers/routes/routes.go | 1 + routers/task/config.go | 10 ++++ services/task/task_config.go | 13 +++++ 6 files changed, 92 insertions(+), 5 deletions(-) diff --git a/models/limit_config.go b/models/limit_config.go index 75dccac9a..154c13ed8 100644 --- a/models/limit_config.go +++ b/models/limit_config.go @@ -53,6 +53,7 @@ type LimitConfig struct { } type LimitConfigVO struct { + Tittle string RefreshRate string Scope string LimitNum int64 diff --git a/models/task_config.go b/models/task_config.go index eee5caea5..44a3bea32 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -54,10 +54,10 @@ type TaskConfig struct { } type TaskConfigWithLimit struct { - TaskCode string + TaskCode string `binding:"Required;MaxSize(256)"` Tittle string AwardType string - AwardAmount int64 + AwardAmount int64 `binding:"Required;MaxSize(256)"` Creator string CreatedUnix timeutil.TimeStamp Limiters []LimitConfigVO @@ -90,3 +90,68 @@ func GetTaskConfigList() ([]*TaskConfig, error) { } return r, nil } + +func AddTaskConfig(config TaskConfigWithLimit, doer *User) error { + sess := x.NewSession() + defer sess.Close() + + //delete old task config + p := &TaskConfig{ + TaskCode: config.TaskCode, + } + _, err := sess.Delete(p) + if err != nil { + sess.Rollback() + return err + } + + //add new config + t := &TaskConfig{ + TaskCode: config.TaskCode, + Tittle: config.Tittle, + AwardType: config.AwardType, + AwardAmount: config.AwardAmount, + CreatorId: doer.ID, + CreatorName: doer.Name, + } + _, err = sess.Insert(t) + if err != nil { + sess.Rollback() + return err + } + + //delete old limiter config + lp := &LimitConfig{ + LimitType: LimitTypeTask.Name(), + LimitCode: config.TaskCode, + } + _, err = sess.Delete(lp) + if err != nil { + sess.Rollback() + return err + } + + //add new limiter config + if config.Limiters != nil && len(config.Limiters) > 0 { + for _, v := range config.Limiters { + //add new config + l := &LimitConfig{ + Tittle: v.Tittle, + RefreshRate: v.RefreshRate, + Scope: v.Scope, + LimitNum: v.LimitNum, + LimitCode: config.TaskCode, + LimitType: LimitTypeTask.Name(), + CreatorId: doer.ID, + CreatorName: doer.Name, + } + _, err = sess.Insert(l) + if err != nil { + sess.Rollback() + return err + } + } + } + sess.Commit() + return nil +} diff --git a/modules/redis/redis_key/task_redis_key.go b/modules/redis/redis_key/task_redis_key.go index 8d6fb3f6e..4e30688e1 100644 --- a/modules/redis/redis_key/task_redis_key.go +++ b/modules/redis/redis_key/task_redis_key.go @@ -9,6 +9,3 @@ func TaskAccomplishLock(sourceId string, taskType string) string { func TaskConfigList() string { return KeyJoin(TASK_REDIS_PREFIX, "config", "list") } -func TaskConfigWithLimiterList() string { - return KeyJoin(TASK_REDIS_PREFIX, "config", "limiter", "list") -} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index b2393246c..135f4d702 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -1322,6 +1322,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/task/config", func() { m.Get("/list", task.GetTaskConfigList) + m.Post("/add", bindIgnErr(models.TaskConfigWithLimit{}), task.AddTaskConfig) }, reqSignIn) if setting.API.EnableSwagger { diff --git a/routers/task/config.go b/routers/task/config.go index 95db0b7d8..d92d0fb51 100644 --- a/routers/task/config.go +++ b/routers/task/config.go @@ -1,6 +1,7 @@ package task import ( + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/routers/response" "code.gitea.io/gitea/services/task" @@ -15,3 +16,12 @@ func GetTaskConfigList(ctx *context.Context) { } ctx.JSON(http.StatusOK, response.SuccessWithData(r)) } + +func AddTaskConfig(ctx *context.Context, config models.TaskConfigWithLimit) { + err := task.AddTaskConfig(config, ctx.User) + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + ctx.JSON(http.StatusOK, response.Success()) +} diff --git a/services/task/task_config.go b/services/task/task_config.go index 3f2225d2c..fe50647e3 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -2,6 +2,7 @@ package task import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/services/reward/limiter" @@ -76,6 +77,7 @@ func GetTaskConfigWithLimitList() ([]*models.TaskConfigWithLimit, error) { lj := l[j] if lj.LimitCode == li.TaskCode { lv = append(lv, models.LimitConfigVO{ + Tittle: lj.Tittle, RefreshRate: lj.RefreshRate, Scope: lj.Scope, LimitNum: lj.LimitNum, @@ -89,3 +91,14 @@ func GetTaskConfigWithLimitList() ([]*models.TaskConfigWithLimit, error) { } return r, nil } + +func AddTaskConfig(config models.TaskConfigWithLimit, doer *models.User) error { + err := models.AddTaskConfig(config, doer) + if err != nil { + log.Error("add task config error,config:%v err:%v", config, err) + return err + } + redis_client.Del(redis_key.LimitConfig(models.LimitTypeTask)) + redis_client.Del(redis_key.TaskConfigList()) + return nil +} From 2122fbab89f2769fa17b907146d91ca3000638a9 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 10 Jun 2022 10:01:04 +0800 Subject: [PATCH 010/283] #1249 add limiter config edit api --- models/limit_config.go | 51 ++++++++++++++++++++++++++++++- models/task_config.go | 2 +- routers/reward/point/limit.go | 27 ++++++++++++++++ routers/routes/routes.go | 2 ++ services/reward/limiter/config.go | 41 +++++++++++++++++++++++++ services/task/task_config.go | 11 ++----- 6 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 routers/reward/point/limit.go create mode 100644 services/reward/limiter/config.go diff --git a/models/limit_config.go b/models/limit_config.go index 154c13ed8..62ff3bfbe 100644 --- a/models/limit_config.go +++ b/models/limit_config.go @@ -1,6 +1,9 @@ package models -import "code.gitea.io/gitea/modules/timeutil" +import ( + "code.gitea.io/gitea/modules/timeutil" + "xorm.io/builder" +) type LimitType string @@ -57,10 +60,23 @@ type LimitConfigVO struct { RefreshRate string Scope string LimitNum int64 + LimitCode string Creator string CreatedUnix timeutil.TimeStamp } +func (l *LimitConfig) ToLimitConfigVO() *LimitConfigVO { + return &LimitConfigVO{ + Tittle: l.Tittle, + RefreshRate: l.RefreshRate, + Scope: l.Scope, + LimitNum: l.LimitNum, + LimitCode: l.LimitCode, + Creator: l.CreatorName, + CreatedUnix: l.CreatedUnix, + } +} + func GetLimitConfigByLimitType(limitType LimitType) ([]LimitConfig, error) { r := make([]LimitConfig, 0) err := x.Where(" limit_type = ?", limitType.Name()).Find(&r) @@ -71,3 +87,36 @@ func GetLimitConfigByLimitType(limitType LimitType) ([]LimitConfig, error) { } return r, nil } + +func AddLimitConfig(l *LimitConfig) error { + sess := x.NewSession() + defer sess.Close() + + //delete old limit config + cond := builder.NewCond() + cond = cond.And(builder.Eq{"limit_type": l.LimitType}) + cond = cond.And(builder.Eq{"scope": l.Scope}) + if l.LimitCode == "" { + subCond := builder.NewCond() + subCond = subCond.Or(builder.IsNull{"limit_code"}) + subCond = subCond.Or(builder.Eq{"limit_code": ""}) + cond = cond.And(subCond) + } else { + cond = cond.And(builder.Eq{"limit_code": l.LimitCode}) + } + _, err := sess.Where(cond).Delete(&LimitConfig{}) + if err != nil { + sess.Rollback() + return err + } + + //add new config + _, err = sess.Insert(l) + if err != nil { + sess.Rollback() + return err + } + + sess.Commit() + return nil +} diff --git a/models/task_config.go b/models/task_config.go index 44a3bea32..922273c46 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -60,7 +60,7 @@ type TaskConfigWithLimit struct { AwardAmount int64 `binding:"Required;MaxSize(256)"` Creator string CreatedUnix timeutil.TimeStamp - Limiters []LimitConfigVO + Limiters []*LimitConfigVO } func getTaskConfig(t *TaskConfig) (*TaskConfig, error) { diff --git a/routers/reward/point/limit.go b/routers/reward/point/limit.go new file mode 100644 index 000000000..a831169f8 --- /dev/null +++ b/routers/reward/point/limit.go @@ -0,0 +1,27 @@ +package point + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/routers/response" + "code.gitea.io/gitea/services/reward/limiter" + "net/http" +) + +func GetPointLimitConfigList(ctx *context.Context) { + r, err := limiter.GetLimitConfigList(models.LimitTypeRewardPoint) + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + ctx.JSON(http.StatusOK, response.SuccessWithData(r)) +} + +func AddPointLimitConfig(ctx *context.Context, config models.LimitConfigVO) { + err := limiter.AddLimitConfig(&config, ctx.User, models.LimitTypeRewardPoint) + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + ctx.JSON(http.StatusOK, response.Success()) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 135f4d702..89416ba16 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -1318,6 +1318,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/reward/point", func() { m.Get("/account", point.GetPointAccount) + m.Get("/limiter/list", point.GetPointLimitConfigList) + m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) }, reqSignIn) m.Group("/task/config", func() { diff --git a/services/reward/limiter/config.go b/services/reward/limiter/config.go new file mode 100644 index 000000000..12204b2c5 --- /dev/null +++ b/services/reward/limiter/config.go @@ -0,0 +1,41 @@ +package limiter + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" +) + +func GetLimitConfigList(limitType models.LimitType) ([]*models.LimitConfigVO, error) { + r, err := GetLimitersByLimitType(limitType) + if err != nil { + return nil, err + } + result := make([]*models.LimitConfigVO, 0) + for _, v := range r { + result = append(result, v.ToLimitConfigVO()) + } + return result, nil +} + +func AddLimitConfig(config *models.LimitConfigVO, doer *models.User, limitType models.LimitType) error { + r := &models.LimitConfig{ + Tittle: config.Tittle, + RefreshRate: config.RefreshRate, + Scope: config.Scope, + LimitNum: config.LimitNum, + LimitCode: config.LimitCode, + LimitType: limitType.Name(), + CreatorId: doer.ID, + CreatorName: doer.Name, + } + err := models.AddLimitConfig(r) + + if err != nil { + log.Error("add limit config error,config:%v err:%v", config, err) + return err + } + redis_client.Del(redis_key.LimitConfig(limitType)) + return nil +} diff --git a/services/task/task_config.go b/services/task/task_config.go index fe50647e3..0001edc21 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -72,18 +72,11 @@ func GetTaskConfigWithLimitList() ([]*models.TaskConfigWithLimit, error) { Creator: li.CreatorName, CreatedUnix: li.CreatedUnix, } - lv := make([]models.LimitConfigVO, 0) + lv := make([]*models.LimitConfigVO, 0) for j := 0; j < len(l); j++ { lj := l[j] if lj.LimitCode == li.TaskCode { - lv = append(lv, models.LimitConfigVO{ - Tittle: lj.Tittle, - RefreshRate: lj.RefreshRate, - Scope: lj.Scope, - LimitNum: lj.LimitNum, - Creator: lj.CreatorName, - CreatedUnix: lj.CreatedUnix, - }) + lv = append(lv, lj.ToLimitConfigVO()) } } t.Limiters = lv From 5a9d544003671898333f672a3760f4d3ff58a658 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 10 Jun 2022 11:48:26 +0800 Subject: [PATCH 011/283] #1249 add batch config api --- models/task_config.go | 3 +++ routers/routes/routes.go | 18 +++++++++++------- routers/task/config.go | 16 ++++++++++++++++ services/task/task_config.go | 4 ++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/models/task_config.go b/models/task_config.go index 922273c46..cd4329834 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -62,6 +62,9 @@ type TaskConfigWithLimit struct { CreatedUnix timeutil.TimeStamp Limiters []*LimitConfigVO } +type BatchLimitConfigVO struct { + ConfigList []TaskConfigWithLimit +} func getTaskConfig(t *TaskConfig) (*TaskConfig, error) { has, err := x.Get(t) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 89416ba16..32d0a55af 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -589,6 +589,17 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/delete", admin.DeleteNotices) m.Post("/empty", admin.EmptyNotices) }) + + m.Group("/reward/point", func() { + m.Get("/limiter/list", point.GetPointLimitConfigList) + m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) + }) + + m.Group("/task/config", func() { + m.Get("/list", task.GetTaskConfigList) + m.Post("/add", bindIgnErr(models.TaskConfigWithLimit{}), task.AddTaskConfig) + m.Post("/add/batch", bindIgnErr(models.BatchLimitConfigVO{}), task.BatchAddTaskConfig) + }) }, adminReq) // ***** END: Admin ***** @@ -1318,13 +1329,6 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/reward/point", func() { m.Get("/account", point.GetPointAccount) - m.Get("/limiter/list", point.GetPointLimitConfigList) - m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) - }, reqSignIn) - - m.Group("/task/config", func() { - m.Get("/list", task.GetTaskConfigList) - m.Post("/add", bindIgnErr(models.TaskConfigWithLimit{}), task.AddTaskConfig) }, reqSignIn) if setting.API.EnableSwagger { diff --git a/routers/task/config.go b/routers/task/config.go index d92d0fb51..0216ffea0 100644 --- a/routers/task/config.go +++ b/routers/task/config.go @@ -25,3 +25,19 @@ func AddTaskConfig(ctx *context.Context, config models.TaskConfigWithLimit) { } ctx.JSON(http.StatusOK, response.Success()) } +func BatchAddTaskConfig(ctx *context.Context, list models.BatchLimitConfigVO) { + successCount := 0 + failCount := 0 + for _, config := range list.ConfigList { + err := task.AddTaskConfig(config, ctx.User) + if err != nil { + failCount++ + } else { + successCount++ + } + } + r := make(map[string]int, 2) + r["successCount"] = successCount + r["failCount"] = failCount + ctx.JSON(http.StatusOK, response.SuccessWithData(r)) +} diff --git a/services/task/task_config.go b/services/task/task_config.go index 0001edc21..4e02b4972 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -7,6 +7,7 @@ import ( "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/services/reward/limiter" "encoding/json" + "errors" "time" ) @@ -86,6 +87,9 @@ func GetTaskConfigWithLimitList() ([]*models.TaskConfigWithLimit, error) { } func AddTaskConfig(config models.TaskConfigWithLimit, doer *models.User) error { + if config.TaskCode == "" || config.AwardType == "" { + return errors.New("param error") + } err := models.AddTaskConfig(config, doer) if err != nil { log.Error("add task config error,config:%v err:%v", config, err) From f605544640e0f9d157665558dd64b09ba20c8e42 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 16 Jun 2022 17:57:35 +0800 Subject: [PATCH 012/283] #2225 add reward operate notification --- models/reward_operate_record.go | 66 +++++++++++++++++++-- modules/eventsource/manager_run.go | 20 +++++++ modules/redis/redis_client/client.go | 37 ++++++++++++ modules/redis/redis_key/reward_redis_key.go | 4 ++ modules/setting/setting.go | 27 +++++---- routers/routes/routes.go | 1 + services/reward/limiter/limiter.go | 2 + services/reward/notify.go | 47 +++++++++++++++ services/reward/operator.go | 28 ++++++--- services/reward/point/point_operate.go | 7 ++- services/task/task.go | 4 +- web_src/js/features/notification.js | 7 +++ 12 files changed, 221 insertions(+), 29 deletions(-) create mode 100644 services/reward/notify.go diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index b1b9983c3..4c31df03c 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -2,6 +2,7 @@ package models import ( "code.gitea.io/gitea/modules/timeutil" + "strings" "xorm.io/builder" ) @@ -25,10 +26,49 @@ func (r RewardType) Name() string { return "" } } +func (r RewardType) Show() string { + switch r { + case RewardTypePoint: + return "积分" + default: + return "" + } +} +func GetRewardTypeInstance(s string) RewardType { + switch s { + case RewardTypePoint.Name(): + return RewardTypePoint + default: + return "" + } +} + +type RewardOperateType string + +func (r RewardOperateType) Name() string { + switch r { + case OperateTypeIncrease: + return "INCREASE" + case OperateTypeDecrease: + return "DECREASE" + default: + return "" + } +} +func (r RewardOperateType) Show() string { + switch r { + case OperateTypeIncrease: + return "奖励" + case OperateTypeDecrease: + return "扣减" + default: + return "" + } +} const ( - OperateTypeIncrease = "INCREASE" - OperateTypeDecrease = "DECREASE" + OperateTypeIncrease RewardOperateType = "INCREASE" + OperateTypeDecrease RewardOperateType = "DECREASE" ) const ( @@ -37,6 +77,8 @@ const ( OperateStatusFailed = "FAILED" ) +const Semicolon = ";" + type RewardOperateRecord struct { ID int64 `xorm:"pk autoincr"` RecordId string `xorm:"INDEX NOT NULL"` @@ -104,11 +146,27 @@ type RewardOperateContext struct { Reward Reward TargetUserId int64 RequestId string - OperateType string + OperateType RewardOperateType CycleIntervalSeconds int64 } type Reward struct { Amount int64 - Type string + Type RewardType +} + +type UserRewardOperationRedis struct { + UserId int64 + Amount int64 + RewardType RewardType + OperateType RewardOperateType +} + +type UserRewardOperation struct { + UserId int64 + Msg string +} + +func AppendRemark(remark, appendStr string) string { + return strings.TrimPrefix(remark+Semicolon+appendStr, Semicolon) } diff --git a/modules/eventsource/manager_run.go b/modules/eventsource/manager_run.go index 75d3ee5b0..857eaee22 100644 --- a/modules/eventsource/manager_run.go +++ b/modules/eventsource/manager_run.go @@ -5,6 +5,7 @@ package eventsource import ( + "code.gitea.io/gitea/services/reward" "context" "time" @@ -24,8 +25,26 @@ func (m *Manager) Init() { func (m *Manager) Run(ctx context.Context) { then := timeutil.TimeStampNow().Add(-2) timer := time.NewTicker(setting.UI.Notification.EventSourceUpdateTime) + rewardThen := then + rewardTimer := time.NewTicker(setting.UI.Notification.RewardNotifyUpdateTime) loop: for { + select { + case <-rewardTimer.C: + now := timeutil.TimeStampNow().Add(-2) + list := reward.GetRewardOperation(rewardThen, now) + if list != nil { + for _, l := range list { + m.SendMessage(l.UserId, &Event{ + Name: "reward-operation", + Data: l.Msg, + }) + } + } + + rewardThen = now + } + select { case <-ctx.Done(): timer.Stop() @@ -44,6 +63,7 @@ loop: }) } then = now + default: } } m.UnregisterAll() diff --git a/modules/redis/redis_client/client.go b/modules/redis/redis_client/client.go index 21a6da9fb..c5cb936b3 100644 --- a/modules/redis/redis_client/client.go +++ b/modules/redis/redis_client/client.go @@ -130,3 +130,40 @@ func GetInt64(key string) (bool, int64, error) { return true, i, nil } + +func ZAdd(key, value string, score float64) error { + redisClient := labelmsg.Get() + defer redisClient.Close() + + _, err := redisClient.Do("ZADD", key, score, value) + if err != nil { + return err + } + return nil +} + +func ZRangeByScore(key string, min, max float64) ([]string, error) { + redisClient := labelmsg.Get() + defer redisClient.Close() + + reply, err := redisClient.Do("ZRANGEBYSCORE", key, min, max) + if err != nil { + return nil, err + } + if reply == nil { + return nil, err + } + s, _ := redis.Strings(reply, nil) + return s, nil +} + +func ZRemRangeByScore(key string, min, max float64) error { + redisClient := labelmsg.Get() + defer redisClient.Close() + + _, err := redisClient.Do("ZREMRANGEBYSCORE", key, min, max) + if err != nil { + return err + } + return nil +} diff --git a/modules/redis/redis_key/reward_redis_key.go b/modules/redis/redis_key/reward_redis_key.go index df8c0ca16..add304db4 100644 --- a/modules/redis/redis_key/reward_redis_key.go +++ b/modules/redis/redis_key/reward_redis_key.go @@ -5,3 +5,7 @@ const REWARD_REDIS_PREFIX = "reward" func RewardSendLock(requestId string, sourceType string) string { return KeyJoin(REWARD_REDIS_PREFIX, requestId, sourceType, "send") } + +func RewardOperateNotification() string { + return KeyJoin(REWARD_REDIS_PREFIX, "operate", "notification") +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5c87b68c5..595c51286 100755 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -184,10 +184,11 @@ var ( UseServiceWorker bool Notification struct { - MinTimeout time.Duration - TimeoutStep time.Duration - MaxTimeout time.Duration - EventSourceUpdateTime time.Duration + MinTimeout time.Duration + TimeoutStep time.Duration + MaxTimeout time.Duration + EventSourceUpdateTime time.Duration + RewardNotifyUpdateTime time.Duration } `ini:"ui.notification"` Admin struct { @@ -221,15 +222,17 @@ var ( Themes: []string{`gitea`, `arc-green`}, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, Notification: struct { - MinTimeout time.Duration - TimeoutStep time.Duration - MaxTimeout time.Duration - EventSourceUpdateTime time.Duration + MinTimeout time.Duration + TimeoutStep time.Duration + MaxTimeout time.Duration + EventSourceUpdateTime time.Duration + RewardNotifyUpdateTime time.Duration }{ - MinTimeout: 10 * time.Second, - TimeoutStep: 10 * time.Second, - MaxTimeout: 60 * time.Second, - EventSourceUpdateTime: 10 * time.Second, + MinTimeout: 10 * time.Second, + TimeoutStep: 10 * time.Second, + MaxTimeout: 60 * time.Second, + EventSourceUpdateTime: 10 * time.Second, + RewardNotifyUpdateTime: 3 * time.Second, }, Admin: struct { UserPagingNum int diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 32d0a55af..31075742c 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -325,6 +325,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/dashboard", routers.Dashboard) go routers.SocketManager.Run() m.Get("/action/notification", routers.ActionNotification) + m.Get("/reward/notification", routers.ActionNotification) m.Get("/recommend/org", routers.RecommendOrgFromPromote) m.Get("/recommend/repo", routers.RecommendRepoFromPromote) m.Get("/recommend/userrank/:index", routers.GetUserRankFromPromote) diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go index fafaab9cb..f094e3a43 100644 --- a/services/reward/limiter/limiter.go +++ b/services/reward/limiter/limiter.go @@ -163,11 +163,13 @@ func (l *limiterRunner) limit(r models.LimitConfig) error { realAmount := l.amount - exceed redis_client.IncrBy(redisKey, -1*exceed) l.resultMap[l.index] = newLimitResult(true, l.amount, realAmount) + return nil case JustReject: redis_client.IncrBy(redisKey, -1*l.amount) return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) case PermittedOnce: l.resultMap[l.index] = newLimitResult(false, l.amount, l.amount) + return nil } } diff --git a/services/reward/notify.go b/services/reward/notify.go new file mode 100644 index 000000000..aa18fbe39 --- /dev/null +++ b/services/reward/notify.go @@ -0,0 +1,47 @@ +package reward + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/timeutil" + "encoding/json" + "fmt" + "time" +) + +func NotifyRewardOperation(userId, amount int64, rewardType models.RewardType, operateType models.RewardOperateType) { + data := &models.UserRewardOperationRedis{ + UserId: userId, + Amount: amount, + RewardType: rewardType, + OperateType: operateType, + } + b, _ := json.Marshal(data) + redis_client.ZAdd(redis_key.RewardOperateNotification(), string(b), float64(time.Now().UnixMilli())) +} + +func GetRewardOperation(since, until timeutil.TimeStamp) []models.UserRewardOperation { + list, err := redis_client.ZRangeByScore(redis_key.RewardOperateNotification(), float64(since*1000), float64(until*1000)) + if err != nil { + return nil + } + if len(list) == 0 { + return nil + } + r := make([]models.UserRewardOperation, len(list)) + for _, v := range list { + t := models.UserRewardOperationRedis{} + json.Unmarshal([]byte(v), &t) + r = append(r, models.UserRewardOperation{ + UserId: t.UserId, + Msg: GetRewardOperateMsg(t), + }) + } + redis_client.ZRemRangeByScore(redis_key.RewardOperateNotification(), float64(since*1000), float64(until*1000)) + return r +} + +func GetRewardOperateMsg(u models.UserRewardOperationRedis) string { + return u.OperateType.Show() + fmt.Sprint(u.Amount) + u.RewardType.Show() +} diff --git a/services/reward/operator.go b/services/reward/operator.go index 8d24ed055..40c093b67 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -17,17 +17,21 @@ var RewardOperatorMap = map[string]RewardOperator{ } type RewardOperator interface { - IsLimited(ctx models.RewardOperateContext) bool - Operate(ctx models.RewardOperateContext) error + IsLimited(ctx *models.RewardOperateContext) bool + Operate(ctx *models.RewardOperateContext) error } -func Send(ctx models.RewardOperateContext) error { +func Send(ctx *models.RewardOperateContext) error { defer func() { if err := recover(); err != nil { combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) log.Error("PANIC:%v", combinedErr) } }() + if !checkRewardOperationParam(ctx) { + log.Error("send reward error,param incorrect") + return errors.New("param incorrect") + } //add lock var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardSendLock(ctx.RequestId, ctx.SourceType)) isOk, err := rewardLock.Lock(3 * time.Second) @@ -79,12 +83,20 @@ func Send(ctx models.RewardOperateContext) error { //if not a cycle operate,update status to success if ctx.CycleIntervalSeconds == 0 { updateAwardOperateRecordStatus(ctx.SourceType, ctx.RequestId, models.OperateStatusOperating, models.OperateStatusSucceeded) + NotifyRewardOperation(ctx.TargetUserId, ctx.Reward.Amount, ctx.Reward.Type, ctx.OperateType) } return nil } -func GetOperator(rewardType string) RewardOperator { - return RewardOperatorMap[rewardType] +func checkRewardOperationParam(ctx *models.RewardOperateContext) bool { + if ctx.Reward.Type == "" { + return false + } + return true +} + +func GetOperator(rewardType models.RewardType) RewardOperator { + return RewardOperatorMap[rewardType.Name()] } func isHandled(sourceType string, requestId string) (bool, error) { @@ -99,16 +111,16 @@ func isHandled(sourceType string, requestId string) (bool, error) { } -func initAwardOperateRecord(ctx models.RewardOperateContext) (string, error) { +func initAwardOperateRecord(ctx *models.RewardOperateContext) (string, error) { record := &models.RewardOperateRecord{ RecordId: util.UUID(), UserId: ctx.TargetUserId, Amount: ctx.Reward.Amount, - RewardType: ctx.Reward.Type, + RewardType: ctx.Reward.Type.Name(), SourceType: ctx.SourceType, SourceId: ctx.SourceId, RequestId: ctx.RequestId, - OperateType: ctx.OperateType, + OperateType: ctx.OperateType.Name(), CycleIntervalSeconds: ctx.CycleIntervalSeconds, Status: models.OperateStatusOperating, Remark: ctx.Remark, diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index eaebdf764..38b6b5384 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -17,19 +17,20 @@ const LossMsg = "达到奖励上限,应得%d积分,实得%d积分" type PointOperator struct { } -func (operator *PointOperator) IsLimited(ctx models.RewardOperateContext) bool { +func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) bool { realAmount, err := limiter.CheckLimitWithFillUp(ctx.SourceType, models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount) if err != nil { return true } if realAmount < ctx.Reward.Amount { - ctx.Remark = ctx.Remark + ";" + fmt.Sprintf(LossMsg, ctx.Reward.Amount, realAmount) + ctx.Remark = models.AppendRemark(ctx.Remark, fmt.Sprintf(LossMsg, ctx.Reward.Amount, realAmount)) + ctx.Reward.Amount = realAmount } return false } -func (operator *PointOperator) Operate(ctx models.RewardOperateContext) error { +func (operator *PointOperator) Operate(ctx *models.RewardOperateContext) error { a, err := account.GetAccount(ctx.TargetUserId) if err != nil || a == nil { return errors.New("get account error") diff --git a/services/task/task.go b/services/task/task.go index 737094b4e..cd6ca830e 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -51,12 +51,12 @@ func accomplish(userId int64, taskType string) error { } //reward - reward.Send(models.RewardOperateContext{ + reward.Send(&models.RewardOperateContext{ SourceType: models.SourceTypeAccomplishTask, SourceId: logId, Reward: models.Reward{ Amount: config.AwardAmount, - Type: config.AwardType, + Type: models.GetRewardTypeInstance(config.AwardType), }, TargetUserId: userId, RequestId: logId, diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js index 8b843e980..6f362eee6 100644 --- a/web_src/js/features/notification.js +++ b/web_src/js/features/notification.js @@ -45,6 +45,13 @@ export function initNotificationCount() { console.error(error); } }); + source.addEventListener('reward-operation', async (e) => { + try { + console.log(e.data); + } catch (error) { + console.error(error); + } + }); source.addEventListener('logout', async (e) => { if (e.data !== 'here') { return; From dc58c5493e45832ddaaf5939b1ba468d4ea4e285 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 21 Jun 2022 16:50:15 +0800 Subject: [PATCH 013/283] #2225 add clloudbrain deduct task --- models/cloudbrain.go | 27 ++-- models/models.go | 2 +- models/point_periodic_task.go | 28 ---- models/reward_operate_record.go | 88 ++++++++---- models/reward_periodic_task.go | 114 ++++++++++++++++ modules/auth/modelarts.go | 3 + modules/context/point.go | 19 +++ modules/cron/tasks_basic.go | 26 ++++ modules/modelarts/modelarts.go | 2 + modules/redis/redis_key/reward_redis_key.go | 9 +- modules/setting/setting.go | 8 +- routers/repo/cloudbrain.go | 30 +++- routers/repo/modelarts.go | 32 ++++- routers/routes/routes.go | 12 +- services/reward/cloubrain_deduct.go | 128 ++++++++++++++++++ services/reward/operator.go | 143 ++++++++++++++++---- services/reward/period_task.go | 103 ++++++++++++++ services/reward/point/point_operate.go | 3 +- services/task/task.go | 2 +- 19 files changed, 675 insertions(+), 104 deletions(-) delete mode 100644 models/point_periodic_task.go create mode 100644 models/reward_periodic_task.go create mode 100644 modules/context/point.go create mode 100644 services/reward/cloubrain_deduct.go create mode 100644 services/reward/period_task.go diff --git a/models/cloudbrain.go b/models/cloudbrain.go index 810e68d30..9b30c4200 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -531,11 +531,12 @@ type ResourceSpecs struct { } type ResourceSpec struct { - Id int `json:"id"` - CpuNum int `json:"cpu"` - GpuNum int `json:"gpu"` - MemMiB int `json:"memMiB"` - ShareMemMiB int `json:"shareMemMiB"` + Id int `json:"id"` + CpuNum int `json:"cpu"` + GpuNum int `json:"gpu"` + MemMiB int `json:"memMiB"` + ShareMemMiB int `json:"shareMemMiB"` + UnitPrice int64 `json:"unitPrice"` } type FlavorInfos struct { @@ -543,9 +544,10 @@ type FlavorInfos struct { } type FlavorInfo struct { - Id int `json:"id"` - Value string `json:"value"` - Desc string `json:"desc"` + Id int `json:"id"` + Value string `json:"value"` + Desc string `json:"desc"` + UnitPrice int64 `json:"unitPrice"` } type ImageInfosModelArts struct { @@ -1692,3 +1694,12 @@ func CloudbrainAll(opts *CloudbrainsOptions) ([]*CloudbrainInfo, int64, error) { return cloudbrains, count, nil } + +func GetStartedCloudbrainTaskByUpdatedUnix(startTime, endTime time.Time) ([]Cloudbrain, error) { + r := make([]Cloudbrain, 0) + err := x.Where("updated_unix >= ? and updated_unix <= ? and start_time > 0", startTime.Unix(), endTime.Unix()).Find(&r) + if err != nil { + return nil, err + } + return r, nil +} diff --git a/models/models.go b/models/models.go index 59e7a3a48..c6c0d6610 100755 --- a/models/models.go +++ b/models/models.go @@ -148,7 +148,7 @@ func init() { new(TaskAccomplishLog), new(RewardOperateRecord), new(LimitConfig), - new(PeriodicTask), + new(RewardPeriodicTask), new(PointAccountLog), new(PointAccount), ) diff --git a/models/point_periodic_task.go b/models/point_periodic_task.go deleted file mode 100644 index 0d4297f2f..000000000 --- a/models/point_periodic_task.go +++ /dev/null @@ -1,28 +0,0 @@ -package models - -import "code.gitea.io/gitea/modules/timeutil" - -type PeriodicTaskStatus int - -// Possible PeriodicTaskStatus types. -const ( - PeriodicTaskStatusRunning PointAccountStatus = iota + 1 // 1 - PeriodicTaskStatusSuccess // 2 - PeriodicTaskStatusFailed // 3 -) - -type PeriodicTask struct { - ID int64 `xorm:"pk autoincr"` - Type string `xorm:"NOT NULL"` - OperateRecordId int64 `xorm:"INDEX NOT NULL"` - IntervalSecond int64 `xorm:"NOT NULL"` - PointsAmount int64 `xorm:"NOT NULL"` - NextExecuteTime timeutil.TimeStamp - SuccessCount int `xorm:"NOT NULL default 0"` - FailedCount int `xorm:"NOT NULL default 0"` - Status string `xorm:"NOT NULL"` - ExitCode string - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - FinishedUnix timeutil.TimeStamp `xorm:"INDEX"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` -} diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 4c31df03c..d3b2e0a10 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -6,12 +6,27 @@ import ( "xorm.io/builder" ) +type SourceType string + const ( - SourceTypeAccomplishTask string = "ACCOMPLISH_TASK" - SourceTypeAdminOperate = "ADMIN_OPERATE" - SourceTypeRunCloudbrainTask = "RUN_CLOUBRAIN_TASK" + SourceTypeAccomplishTask SourceType = "ACCOMPLISH_TASK" + SourceTypeAdminOperate SourceType = "ADMIN_OPERATE" + SourceTypeRunCloudbrainTask SourceType = "RUN_CLOUDBRAIN_TASK" ) +func (r SourceType) Name() string { + switch r { + case SourceTypeAccomplishTask: + return "ACCOMPLISH_TASK" + case SourceTypeAdminOperate: + return "ADMIN_OPERATE" + case SourceTypeRunCloudbrainTask: + return "RUN_CLOUDBRAIN_TASK" + default: + return "" + } +} + type RewardType string const ( @@ -66,6 +81,17 @@ func (r RewardOperateType) Show() string { } } +func GetRewardOperateTypeInstance(s string) RewardOperateType { + switch s { + case OperateTypeIncrease.Name(): + return OperateTypeIncrease + case OperateTypeDecrease.Name(): + return OperateTypeDecrease + default: + return "" + } +} + const ( OperateTypeIncrease RewardOperateType = "INCREASE" OperateTypeDecrease RewardOperateType = "DECREASE" @@ -80,20 +106,19 @@ const ( const Semicolon = ";" type RewardOperateRecord struct { - ID int64 `xorm:"pk autoincr"` - RecordId string `xorm:"INDEX NOT NULL"` - UserId int64 `xorm:"INDEX NOT NULL"` - Amount int64 `xorm:"NOT NULL"` - RewardType string `xorm:"NOT NULL"` - SourceType string `xorm:"NOT NULL"` - SourceId string `xorm:"INDEX NOT NULL"` - RequestId string `xorm:"INDEX NOT NULL"` - OperateType string `xorm:"NOT NULL"` - CycleIntervalSeconds int64 `xorm:"NOT NULL default 0"` - Status string `xorm:"NOT NULL"` - Remark string - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + ID int64 `xorm:"pk autoincr"` + RecordId string `xorm:"INDEX NOT NULL"` + UserId int64 `xorm:"INDEX NOT NULL"` + Amount int64 `xorm:"NOT NULL"` + RewardType string `xorm:"NOT NULL"` + SourceType string `xorm:"NOT NULL"` + SourceId string `xorm:"INDEX NOT NULL"` + RequestId string `xorm:"INDEX NOT NULL"` + OperateType string `xorm:"NOT NULL"` + Status string `xorm:"NOT NULL"` + Remark string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error) { @@ -106,10 +131,18 @@ func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error return tl, nil } -func GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId string) (*RewardOperateRecord, error) { +func GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId, operateType string) (*RewardOperateRecord, error) { + t := &RewardOperateRecord{ + SourceType: sourceType, + RequestId: requestId, + OperateType: operateType, + } + return getPointOperateRecord(t) +} + +func GetPointOperateRecordByRecordId(recordId string) (*RewardOperateRecord, error) { t := &RewardOperateRecord{ - SourceType: sourceType, - RequestId: requestId, + RecordId: recordId, } return getPointOperateRecord(t) } @@ -140,14 +173,13 @@ func SumRewardAmountInTaskPeriod(rewardType string, sourceType string, userId in } type RewardOperateContext struct { - SourceType string - SourceId string - Remark string - Reward Reward - TargetUserId int64 - RequestId string - OperateType RewardOperateType - CycleIntervalSeconds int64 + SourceType SourceType + SourceId string + Remark string + Reward Reward + TargetUserId int64 + RequestId string + OperateType RewardOperateType } type Reward struct { diff --git a/models/reward_periodic_task.go b/models/reward_periodic_task.go new file mode 100644 index 000000000..e6ebd17c2 --- /dev/null +++ b/models/reward_periodic_task.go @@ -0,0 +1,114 @@ +package models + +import ( + "code.gitea.io/gitea/modules/timeutil" + "time" +) + +type PeriodicTaskStatus int + +const ( + PeriodicTaskStatusRunning = iota + 1 // 1 + PeriodicTaskStatusFinished // 2 +) + +type PeriodType string + +const ( + PeriodType30MinutesFree1HourCost PeriodType = "30MF1HC" +) + +func (r PeriodType) Name() string { + switch r { + case PeriodType30MinutesFree1HourCost: + return "30MF1HC" + default: + return "" + } +} + +type RewardPeriodicTask struct { + ID int64 `xorm:"pk autoincr"` + OperateRecordId string `xorm:"INDEX NOT NULL"` + DelaySeconds int64 + IntervalSeconds int64 + Amount int64 `xorm:"NOT NULL"` + NextExecuteTime timeutil.TimeStamp `xorm:"INDEX NOT NULL"` + SuccessCount int `xorm:"NOT NULL default 0"` + Status int `xorm:"NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + FinishedUnix timeutil.TimeStamp `xorm:"INDEX"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +type StartPeriodicTaskOpts struct { + SourceType SourceType + SourceId string + Remark string + TargetUserId int64 + RequestId string + OperateType RewardOperateType + Delay time.Duration + Interval time.Duration + UnitAmount int64 + RewardType RewardType + StartTime time.Time +} + +func InsertPeriodicTask(tl *RewardPeriodicTask) (int64, error) { + return x.Insert(tl) +} + +func GetRunningRewardTask(now time.Time) ([]RewardPeriodicTask, error) { + r := make([]RewardPeriodicTask, 0) + err := x.Where("next_execute_time <= ? and status = ?", now.Unix(), PeriodicTaskStatusRunning).Find(&r) + if err != nil { + return nil, err + } + return r, err +} + +func IncrRewardTaskSuccessCount(t RewardPeriodicTask, count int64, nextTime timeutil.TimeStamp) error { + sess := x.NewSession() + defer sess.Close() + _, err := sess.Exec("update reward_periodic_task set success_count = success_count + ? , next_execute_time = ?, updated_unix = ? where id = ?", count, nextTime, timeutil.TimeStampNow(), t.ID) + if err != nil { + sess.Rollback() + return err + } + _, err = sess.Exec("update reward_operate_record set amount = amount + ? ,updated_unix = ? where record_id = ?", count*t.Amount, timeutil.TimeStampNow(), t.OperateRecordId) + if err != nil { + sess.Rollback() + return err + } + sess.Commit() + return nil +} + +func GetPeriodicTaskBySourceIdAndType(sourceType SourceType, sourceId string, operateType RewardOperateType) (*RewardPeriodicTask, error) { + r := RewardPeriodicTask{} + _, err := x.SQL("select rpt.* from reward_periodic_task rpt "+ + "inner join reward_operate_record ror on rpt.operate_record_id = ror.record_id"+ + " where ror.source_type = ? and source_id = ? and operate_type = ? ", sourceType.Name(), sourceId, operateType.Name()).Get(&r) + if err != nil { + return nil, err + } + return &r, nil +} + +func StopPeriodicTask(taskId int64, operateRecordId string, stopTime time.Time) error { + sess := x.NewSession() + defer sess.Close() + _, err := sess.Where("id = ? and status = ?", taskId, PeriodicTaskStatusRunning).Update(&RewardPeriodicTask{Status: PeriodicTaskStatusFinished, FinishedUnix: timeutil.TimeStamp(stopTime.Unix())}) + if err != nil { + sess.Rollback() + return err + } + _, err = sess.Where("record_id = ? and status = ?", operateRecordId, OperateStatusOperating).Update(&RewardOperateRecord{Status: OperateStatusSucceeded}) + if err != nil { + sess.Rollback() + return err + } + sess.Commit() + return nil +} diff --git a/modules/auth/modelarts.go b/modules/auth/modelarts.go index ce41f5d1e..0cbed45a6 100755 --- a/modules/auth/modelarts.go +++ b/modules/auth/modelarts.go @@ -22,6 +22,7 @@ type CreateModelArtsNotebookForm struct { Description string `form:"description"` Flavor string `form:"flavor" binding:"Required"` ImageId string `form:"image_id" binding:"Required"` + ResourceSpecId int `form:"resource_spec_id"` } func (f *CreateModelArtsNotebookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { @@ -46,6 +47,7 @@ type CreateModelArtsTrainJobForm struct { VersionName string `form:"version_name" binding:"Required"` FlavorName string `form:"flaver_names" binding:"Required"` EngineName string `form:"engine_names" binding:"Required"` + ResourceSpecId int `form:"resource_spec_id"` } type CreateModelArtsInferenceJobForm struct { @@ -71,6 +73,7 @@ type CreateModelArtsInferenceJobForm struct { ModelName string `form:"model_name" binding:"Required"` ModelVersion string `form:"model_version" binding:"Required"` CkptName string `form:"ckpt_name" binding:"Required"` + ResourceSpecId int `form:"resource_spec_id"` } func (f *CreateModelArtsTrainJobForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { diff --git a/modules/context/point.go b/modules/context/point.go new file mode 100644 index 000000000..9fbff61be --- /dev/null +++ b/modules/context/point.go @@ -0,0 +1,19 @@ +package context + +import ( + "code.gitea.io/gitea/services/reward/point/account" + "gitea.com/macaron/macaron" +) + +// PointAccount returns a macaron to get request user's point account +func PointAccount() macaron.Handler { + return func(ctx *Context) { + a, err := account.GetAccount(ctx.User.ID) + if err != nil { + ctx.ServerError("GetPointAccount", err) + return + } + ctx.Data["PointAccount"] = a + ctx.Next() + } +} diff --git a/modules/cron/tasks_basic.go b/modules/cron/tasks_basic.go index b3a6c02a1..39100594d 100755 --- a/modules/cron/tasks_basic.go +++ b/modules/cron/tasks_basic.go @@ -5,6 +5,7 @@ package cron import ( + "code.gitea.io/gitea/services/reward" "context" "time" @@ -207,6 +208,28 @@ func registerSyncCloudbrainStatus() { }) } +func registerRewardPeriodTask() { + RegisterTaskFatal("reward_period_task", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 5m", + }, func(ctx context.Context, _ *models.User, _ Config) error { + reward.StartRewardTask() + return nil + }) +} + +func registerCloudbrainPointDeductTask() { + RegisterTaskFatal("cloudbrain_point_deduct_task", &BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@every 1m", + }, func(ctx context.Context, _ *models.User, _ Config) error { + reward.StartCloudbrainPointDeductTask() + return nil + }) +} + func initBasicTasks() { registerUpdateMirrorTask() registerRepoHealthCheck() @@ -227,4 +250,7 @@ func initBasicTasks() { registerSyncCloudbrainStatus() registerHandleOrgStatistic() + + registerRewardPeriodTask() + registerCloudbrainPointDeductTask() } diff --git a/modules/modelarts/modelarts.go b/modules/modelarts/modelarts.go index 78b40fd56..de5c392cd 100755 --- a/modules/modelarts/modelarts.go +++ b/modules/modelarts/modelarts.go @@ -96,6 +96,7 @@ type GenerateTrainJobReq struct { VersionCount int EngineName string TotalVersionCount int + ResourceSpecId int } type GenerateInferenceJobReq struct { @@ -127,6 +128,7 @@ type GenerateInferenceJobReq struct { ModelVersion string CkptName string ResultUrl string + ResourceSpecId int } type VersionInfo struct { diff --git a/modules/redis/redis_key/reward_redis_key.go b/modules/redis/redis_key/reward_redis_key.go index add304db4..f6c9480a9 100644 --- a/modules/redis/redis_key/reward_redis_key.go +++ b/modules/redis/redis_key/reward_redis_key.go @@ -1,11 +1,16 @@ package redis_key +import "fmt" + const REWARD_REDIS_PREFIX = "reward" -func RewardSendLock(requestId string, sourceType string) string { - return KeyJoin(REWARD_REDIS_PREFIX, requestId, sourceType, "send") +func RewardOperateLock(requestId string, sourceType string, operateType string) string { + return KeyJoin(REWARD_REDIS_PREFIX, requestId, sourceType, operateType, "send") } func RewardOperateNotification() string { return KeyJoin(REWARD_REDIS_PREFIX, "operate", "notification") } +func RewardTaskRunningLock(taskId int64) string { + return KeyJoin(REWARD_REDIS_PREFIX, "periodic_task", fmt.Sprint(taskId), "lock") +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 595c51286..b5ffe6eab 100755 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -548,6 +548,9 @@ var ( WechatQRCodeExpireSeconds int WechatAuthSwitch bool + //point config + CloudBrainTaskPointPaySwitch bool + //nginx proxy PROXYURL string RadarMap = struct { @@ -1374,7 +1377,10 @@ func NewContext() { WechatAppId = sec.Key("APP_ID").MustString("wxba77b915a305a57d") WechatAppSecret = sec.Key("APP_SECRET").MustString("e48e13f315adc32749ddc7057585f198") WechatQRCodeExpireSeconds = sec.Key("QR_CODE_EXPIRE_SECONDS").MustInt(120) - WechatAuthSwitch = sec.Key("AUTH_SWITCH").MustBool(true) + WechatAuthSwitch = sec.Key("AUTH_SWITCH").MustBool(false) + + sec = Cfg.Section("point") + CloudBrainTaskPointPaySwitch = sec.Key("CLOUDBRAIN_PAY_SWITCH").MustBool(false) SetRadarMapConfig() diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 7ed6fa6ef..b4d532ab0 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -2,6 +2,7 @@ package repo import ( "bufio" + "code.gitea.io/gitea/services/reward" "encoding/json" "errors" "fmt" @@ -229,6 +230,13 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { command = commandTrain } + if !reward.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) + cloudBrainNewDataPrepare(ctx) + ctx.RenderWithErr("point balance not enough", tpl, &form) + return + } + tasks, err := models.GetCloudbrainsByDisplayJobName(repo.ID, jobType, displayJobName) if err == nil { if len(tasks) != 0 { @@ -308,6 +316,13 @@ func CloudBrainRestart(ctx *context.Context) { var status = string(models.JobWaiting) task := ctx.Cloudbrain for { + if !reward.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) + resultCode = "-1" + errorMsg = "insufficient points balance" + break + } + if task.Status != string(models.JobStopped) && task.Status != string(models.JobSucceeded) && task.Status != string(models.JobFailed) { log.Error("the job(%s) is not stopped", task.JobName, ctx.Data["MsgID"]) resultCode = "-1" @@ -842,7 +857,6 @@ func CloudBrainStop(ctx *context.Context) { errorMsg = "system error" break } - status = task.Status break } @@ -1845,6 +1859,13 @@ func BenchMarkAlgorithmCreate(ctx *context.Context, form auth.CreateCloudBrainFo repo := ctx.Repo.Repository + if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) + cloudBrainNewDataPrepare(ctx) + ctx.RenderWithErr("point balance not enough", tplCloudBrainBenchmarkNew, &form) + return + } + tasks, err := models.GetCloudbrainsByDisplayJobName(repo.ID, string(models.JobTypeBenchmark), displayJobName) if err == nil { if len(tasks) != 0 { @@ -2000,6 +2021,13 @@ func ModelBenchmarkCreate(ctx *context.Context, form auth.CreateCloudBrainForm) tpl := tplCloudBrainBenchmarkNew command := cloudbrain.Command + if !reward.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) + cloudBrainNewDataPrepare(ctx) + ctx.RenderWithErr("point balance not enough", tpl, &form) + return + } + tasks, err := models.GetCloudbrainsByDisplayJobName(repo.ID, jobType, displayJobName) if err == nil { if len(tasks) != 0 { diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index 95ca8df62..dea996a50 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -2,6 +2,7 @@ package repo import ( "archive/zip" + "code.gitea.io/gitea/services/reward" "encoding/json" "errors" "fmt" @@ -204,7 +205,14 @@ func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm flavor := form.Flavor imageId := form.ImageId repo := ctx.Repo.Repository + resourceSpecId := form.ResourceSpecId + if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeDebug), resourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) + cloudBrainNewDataPrepare(ctx) + ctx.RenderWithErr("point balance not enough", tplModelArtsNotebookNew, &form) + return + } count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID) if err != nil { log.Error("GetCloudbrainNotebookCountByUserID failed:%v", err, ctx.Data["MsgID"]) @@ -418,6 +426,13 @@ func NotebookManage(ctx *context.Context) { errorMsg = "you have no right to restart the job" break } + if !reward.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) + resultCode = "-1" + errorMsg = "point balance not enough" + break + return + } count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID) if err != nil { @@ -985,7 +1000,14 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) FlavorName := form.FlavorName VersionCount := modelarts.VersionCount EngineName := form.EngineName + resourceSpecId := form.ResourceSpecId + if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeTrain), resourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) + cloudBrainNewDataPrepare(ctx) + ctx.RenderWithErr("point balance not enough", tplModelArtsTrainJobNew, &form) + return + } count, err := models.GetCloudbrainTrainJobCountByUserID(ctx.User.ID) if err != nil { log.Error("GetCloudbrainTrainJobCountByUserID failed:%v", err, ctx.Data["MsgID"]) @@ -1161,6 +1183,7 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) EngineName: EngineName, VersionCount: VersionCount, TotalVersionCount: modelarts.TotalVersionCount, + ResourceSpecId: resourceSpecId, } //将params转换Parameters.Parameter,出错时返回给前端 @@ -1716,7 +1739,6 @@ func TrainJobStop(ctx *context.Context) { ctx.RenderWithErr(err.Error(), tplModelArtsTrainJobIndex, nil) return } - ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/modelarts/train-job?listType=" + listType) } @@ -1825,9 +1847,16 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference modelName := form.ModelName modelVersion := form.ModelVersion ckptName := form.CkptName + resourceSpecId := form.ResourceSpecId ckptUrl := form.TrainUrl + form.CkptName + if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeInference), resourceSpecId) { + log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) + inferenceJobErrorNewDataPrepare(ctx, form) + ctx.RenderWithErr("point balance not enough", tplModelArtsInferenceJobNew, &form) + return + } count, err := models.GetCloudbrainInferenceJobCountByUserID(ctx.User.ID) if err != nil { log.Error("GetCloudbrainInferenceJobCountByUserID failed:%v", err, ctx.Data["MsgID"]) @@ -1973,6 +2002,7 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference ModelVersion: modelVersion, CkptName: ckptName, ResultUrl: resultObsPath, + ResourceSpecId: resourceSpecId, } err = modelarts.GenerateInferenceJob(ctx, req) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 31075742c..3ce633f93 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -1068,7 +1068,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/models", reqRepoCloudBrainReader, repo.CloudBrainShowModels) m.Get("/download_model", cloudbrain.AdminOrJobCreaterRight, repo.CloudBrainDownloadModel) }) - m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.CloudBrainNew) + m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, context.PointAccount(), repo.CloudBrainNew) m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) m.Group("/benchmark", func() { @@ -1079,7 +1079,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.BenchmarkDel) m.Get("/rate", reqRepoCloudBrainReader, repo.GetRate) }) - m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.CloudBrainBenchmarkNew) + m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, context.PointAccount(), repo.CloudBrainBenchmarkNew) m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainBenchmarkCreate) m.Get("/get_child_types", repo.GetChildTypes) }) @@ -1093,7 +1093,7 @@ func RegisterRoutes(m *macaron.Macaron) { //m.Get("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, repo.TrainJobNewVersion) //m.Post("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreateVersion) }) - m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.CloudBrainTrainJobNew) + m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, context.PointAccount(), repo.CloudBrainTrainJobNew) m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) }) }, context.RepoRef()) @@ -1141,7 +1141,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/:action", reqRepoCloudBrainWriter, repo.NotebookManage) m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.NotebookDel) }) - m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.NotebookNew) + m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, context.PointAccount(), repo.NotebookNew) m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateModelArtsNotebookForm{}), repo.Notebook2Create) }) @@ -1155,7 +1155,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, repo.TrainJobNewVersion) m.Post("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreateVersion) }) - m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.TrainJobNew) + m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, context.PointAccount(), repo.TrainJobNew) m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreate) m.Get("/para-config-list", reqRepoCloudBrainReader, repo.TrainJobGetConfigList) @@ -1168,7 +1168,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/result_download", cloudbrain.AdminOrJobCreaterRightForTrain, repo.ResultDownload) m.Get("/downloadall", repo.DownloadMultiResultFile) }) - m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.InferenceJobNew) + m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, context.PointAccount(), repo.InferenceJobNew) m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateModelArtsInferenceJobForm{}), repo.InferenceJobCreate) }) }, context.RepoRef()) diff --git a/services/reward/cloubrain_deduct.go b/services/reward/cloubrain_deduct.go new file mode 100644 index 000000000..61068a87a --- /dev/null +++ b/services/reward/cloubrain_deduct.go @@ -0,0 +1,128 @@ +package reward + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/reward/point/account" + "encoding/json" + "fmt" + "time" +) + +var ( + ResourceSpecs *models.ResourceSpecs + TrainResourceSpecs *models.ResourceSpecs +) + +//IsPointBalanceEnough check whether the user's point balance is bigger than task unit price +func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int) bool { + if !setting.CloudBrainTaskPointPaySwitch { + return true + } + spec := getResourceSpec(jobType, resourceSpecId) + if spec == nil { + return true + } + a, error := account.GetAccount(targetUserId) + if error != nil { + return false + } + return a.Balance >= spec.UnitPrice + +} + +func StartCloudBrainPointDeductTask(task models.Cloudbrain) { + if !setting.CloudBrainTaskPointPaySwitch { + return + } + + spec := getResourceSpec(task.JobType, task.ResourceSpecId) + if spec == nil || spec.UnitPrice == 0 { + return + } + + StartPeriodicTask(&models.StartPeriodicTaskOpts{ + SourceType: models.SourceTypeRunCloudbrainTask, + SourceId: getCloudBrainPointTaskSourceId(task), + TargetUserId: task.UserID, + RequestId: getCloudBrainPointTaskSourceId(task), + OperateType: models.OperateTypeDecrease, + Delay: 30 * time.Minute, + Interval: 60 * time.Minute, + UnitAmount: spec.UnitPrice, + RewardType: models.RewardTypePoint, + StartTime: time.Unix(int64(task.StartTime), 0), + }) +} + +func StopCloudBrainPointDeductTask(task models.Cloudbrain) { + StopPeriodicTask(models.SourceTypeRunCloudbrainTask, getCloudBrainPointTaskSourceId(task), models.OperateTypeDecrease) +} + +func getCloudBrainPointTaskSourceId(task models.Cloudbrain) string { + return models.SourceTypeRunCloudbrainTask.Name() + "_" + task.JobType + "_" + fmt.Sprint(task.Type) + "_" + fmt.Sprint(task.ID) +} + +func getResourceSpec(jobType string, resourceSpecId int) *models.ResourceSpec { + if jobType == string(models.JobTypeTrain) { + if TrainResourceSpecs == nil { + json.Unmarshal([]byte(setting.TrainResourceSpecs), &TrainResourceSpecs) + } + for _, spec := range TrainResourceSpecs.ResourceSpec { + if resourceSpecId == spec.Id { + return spec + } + } + } else { + if ResourceSpecs == nil { + json.Unmarshal([]byte(setting.ResourceSpecs), &ResourceSpecs) + } + for _, spec := range ResourceSpecs.ResourceSpec { + if resourceSpecId == spec.Id { + return spec + } + } + + } + return nil + +} + +var firstTimeFlag = true + +func StartCloudbrainPointDeductTask() { + defer func() { + if err := recover(); err != nil { + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC:%v", combinedErr) + } + }() + log.Debug("try to run CloudbrainPointDeductTask") + end := time.Now() + start := end.Add(5 * time.Minute) + if firstTimeFlag { + //When it is executed for the first time, it needs to process the tasks of the last 1 hours. + //This is done to prevent the application from hanging for a long time + start = end.Add(1 * time.Hour) + firstTimeFlag = false + } + + taskList, err := models.GetStartedCloudbrainTaskByUpdatedUnix(start, end) + if err != nil { + log.Error("GetStartedCloudbrainTaskByUpdatedUnix error. %v", err) + return + } + if taskList == nil || len(taskList) == 0 { + log.Debug("No cloudbrain task need handled") + return + } + for _, t := range taskList { + if int64(t.StartTime) <= end.Unix() && int64(t.StartTime) >= start.Unix() { + StartCloudBrainPointDeductTask(t) + } + if int64(t.EndTime) <= end.Unix() && int64(t.EndTime) >= start.Unix() { + StopCloudBrainPointDeductTask(t) + } + } +} diff --git a/services/reward/operator.go b/services/reward/operator.go index 40c093b67..50ec01ff3 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -21,7 +21,7 @@ type RewardOperator interface { Operate(ctx *models.RewardOperateContext) error } -func Send(ctx *models.RewardOperateContext) error { +func Operate(ctx *models.RewardOperateContext) error { defer func() { if err := recover(); err != nil { combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) @@ -33,7 +33,7 @@ func Send(ctx *models.RewardOperateContext) error { return errors.New("param incorrect") } //add lock - var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardSendLock(ctx.RequestId, ctx.SourceType)) + var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardOperateLock(ctx.RequestId, ctx.SourceType.Name(), ctx.OperateType.Name())) isOk, err := rewardLock.Lock(3 * time.Second) if err != nil { return err @@ -45,7 +45,7 @@ func Send(ctx *models.RewardOperateContext) error { defer rewardLock.UnLock() //is handled before? - isHandled, err := isHandled(ctx.SourceType, ctx.RequestId) + isHandled, err := isHandled(ctx.SourceType.Name(), ctx.RequestId, ctx.OperateType.Name()) if err != nil { log.Error("reward is handled error,%v", err) return err @@ -61,9 +61,11 @@ func Send(ctx *models.RewardOperateContext) error { return errors.New("operator of reward type is not exist") } - //is limited? - if isLimited := operator.IsLimited(ctx); isLimited { - return nil + if ctx.OperateType == models.OperateTypeIncrease { + //is limited? + if isLimited := operator.IsLimited(ctx); isLimited { + return nil + } } //new reward operate record @@ -76,15 +78,12 @@ func Send(ctx *models.RewardOperateContext) error { //operate if err := operator.Operate(ctx); err != nil { - updateAwardOperateRecordStatus(ctx.SourceType, ctx.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) + updateAwardOperateRecordStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) return err } - //if not a cycle operate,update status to success - if ctx.CycleIntervalSeconds == 0 { - updateAwardOperateRecordStatus(ctx.SourceType, ctx.RequestId, models.OperateStatusOperating, models.OperateStatusSucceeded) - NotifyRewardOperation(ctx.TargetUserId, ctx.Reward.Amount, ctx.Reward.Type, ctx.OperateType) - } + updateAwardOperateRecordStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusSucceeded) + NotifyRewardOperation(ctx.TargetUserId, ctx.Reward.Amount, ctx.Reward.Type, ctx.OperateType) return nil } @@ -99,8 +98,8 @@ func GetOperator(rewardType models.RewardType) RewardOperator { return RewardOperatorMap[rewardType.Name()] } -func isHandled(sourceType string, requestId string) (bool, error) { - _, err := models.GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId) +func isHandled(sourceType string, requestId string, operateType string) (bool, error) { + _, err := models.GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId, operateType) if err != nil { if models.IsErrRecordNotExist(err) { return false, nil @@ -113,17 +112,36 @@ func isHandled(sourceType string, requestId string) (bool, error) { func initAwardOperateRecord(ctx *models.RewardOperateContext) (string, error) { record := &models.RewardOperateRecord{ - RecordId: util.UUID(), - UserId: ctx.TargetUserId, - Amount: ctx.Reward.Amount, - RewardType: ctx.Reward.Type.Name(), - SourceType: ctx.SourceType, - SourceId: ctx.SourceId, - RequestId: ctx.RequestId, - OperateType: ctx.OperateType.Name(), - CycleIntervalSeconds: ctx.CycleIntervalSeconds, - Status: models.OperateStatusOperating, - Remark: ctx.Remark, + RecordId: util.UUID(), + UserId: ctx.TargetUserId, + Amount: ctx.Reward.Amount, + RewardType: ctx.Reward.Type.Name(), + SourceType: ctx.SourceType.Name(), + SourceId: ctx.SourceId, + RequestId: ctx.RequestId, + OperateType: ctx.OperateType.Name(), + Status: models.OperateStatusOperating, + Remark: ctx.Remark, + } + _, err := models.InsertAwardOperateRecord(record) + if err != nil { + return "", err + } + return record.RecordId, nil +} + +func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (string, error) { + record := &models.RewardOperateRecord{ + RecordId: util.UUID(), + UserId: ctx.TargetUserId, + Amount: 0, + RewardType: ctx.RewardType.Name(), + SourceType: ctx.SourceType.Name(), + SourceId: ctx.SourceId, + RequestId: ctx.RequestId, + OperateType: ctx.OperateType.Name(), + Status: models.OperateStatusOperating, + Remark: ctx.Remark, } _, err := models.InsertAwardOperateRecord(record) if err != nil { @@ -139,3 +157,78 @@ func updateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus } return nil } + +func StartPeriodicTaskAsyn(opts *models.StartPeriodicTaskOpts) { + go StartPeriodicTask(opts) +} + +func StartPeriodicTask(opts *models.StartPeriodicTaskOpts) error { + defer func() { + if err := recover(); err != nil { + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC:%v", combinedErr) + } + }() + //add lock + var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardOperateLock(opts.RequestId, opts.SourceType.Name(), opts.OperateType.Name())) + isOk, err := rewardLock.Lock(3 * time.Second) + if err != nil { + return err + } + if !isOk { + log.Info("duplicated operate request,targetUserId=%d requestId=%s", opts.TargetUserId, opts.RequestId) + return nil + } + defer rewardLock.UnLock() + + //is handled before? + isHandled, err := isHandled(opts.SourceType.Name(), opts.RequestId, opts.OperateType.Name()) + if err != nil { + log.Error("operate is handled error,%v", err) + return err + } + if isHandled { + log.Info("operate has been handled,opts=%+v", opts) + return nil + } + //new reward operate record + recordId, err := createPeriodicRewardOperateRecord(opts) + if err != nil { + return err + } + + if err = NewRewardPeriodicTask(recordId, opts); err != nil { + updateAwardOperateRecordStatus(opts.SourceType.Name(), opts.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) + return err + } + return nil +} + +func StopPeriodicTaskAsyn(sourceType models.SourceType, sourceId string, operateType models.RewardOperateType) { + go StopPeriodicTask(sourceType, sourceId, operateType) +} + +func StopPeriodicTask(sourceType models.SourceType, sourceId string, operateType models.RewardOperateType) error { + defer func() { + if err := recover(); err != nil { + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC:%v", combinedErr) + } + }() + task, err := models.GetPeriodicTaskBySourceIdAndType(sourceType, sourceId, operateType) + if err != nil { + log.Error("StopPeriodicTask. GetPeriodicTaskBySourceIdAndType error. %v", err) + return err + } + if task == nil { + log.Info("Periodic task is not exist") + return nil + } + if task.Status == models.PeriodicTaskStatusFinished { + log.Info("Periodic task is finished") + return nil + } + now := time.Now() + RunRewardTask(*task, now) + return models.StopPeriodicTask(task.ID, task.OperateRecordId, now) +} diff --git a/services/reward/period_task.go b/services/reward/period_task.go new file mode 100644 index 000000000..d00e8d0c4 --- /dev/null +++ b/services/reward/period_task.go @@ -0,0 +1,103 @@ +package reward + +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/modules/timeutil" + "fmt" + "time" +) + +func NewRewardPeriodicTask(operateRecordId string, opts *models.StartPeriodicTaskOpts) error { + task := &models.RewardPeriodicTask{} + task.DelaySeconds = int64(opts.Delay.Seconds()) + task.IntervalSeconds = int64(opts.Interval.Seconds()) + task.Amount = opts.UnitAmount + task.OperateRecordId = operateRecordId + task.Status = models.PeriodicTaskStatusRunning + task.NextExecuteTime = timeutil.TimeStamp(opts.StartTime.Add(opts.Delay).Unix()) + + _, err := models.InsertPeriodicTask(task) + return err +} + +func StartRewardTask() { + defer func() { + if err := recover(); err != nil { + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC:%v", combinedErr) + } + }() + log.Debug("try to run reward tasks") + now := time.Now() + taskList, err := models.GetRunningRewardTask(now) + if err != nil { + log.Error("GetRunningRewardTask error. %v", err) + return + } + if taskList == nil || len(taskList) == 0 { + log.Debug("No GetRunningRewardTask need handled") + return + } + for _, t := range taskList { + RunRewardTask(t, now) + } +} + +func RunRewardTask(t models.RewardPeriodicTask, now time.Time) { + lock := redis_lock.NewDistributeLock(redis_key.RewardTaskRunningLock(t.ID)) + isOk, _ := lock.LockWithWait(3*time.Second, 3*time.Second) + if !isOk { + log.Error("get RewardTaskRunningLock failed,t=%+v", t) + return + } + defer lock.UnLock() + record, err := models.GetPointOperateRecordByRecordId(t.OperateRecordId) + if err != nil { + log.Error("RunRewardTask. GetPointOperateRecordByRecordId error. %v", err) + return + } + if record.Status != models.OperateStatusOperating { + log.Info("RunRewardTask. operate record is finished,record=%+v", record) + return + } + n, nextTime := countExecuteTimes(t, now) + if n == 0 { + return + } + //get operator + operator := GetOperator(models.GetRewardTypeInstance(record.RewardType)) + if operator == nil { + log.Error("RunRewardTask. operator of reward type is not exist") + return + } + err = operator.Operate(&models.RewardOperateContext{ + SourceType: models.SourceTypeRunCloudbrainTask, + SourceId: t.OperateRecordId, + Reward: models.Reward{ + Amount: n * t.Amount, + Type: models.GetRewardTypeInstance(record.RewardType), + }, + TargetUserId: record.UserId, + OperateType: models.GetRewardOperateTypeInstance(record.OperateType), + }) + if err != nil { + log.Error("RunRewardTask.operator operate error.%v", err) + return + } + models.IncrRewardTaskSuccessCount(t, n, nextTime) +} + +func countExecuteTimes(t models.RewardPeriodicTask, now time.Time) (int64, timeutil.TimeStamp) { + interval := t.IntervalSeconds + nextTime := int64(t.NextExecuteTime) + if nextTime > now.Unix() { + return 0, 0 + } + diff := now.Unix() - nextTime + n := diff/interval + 1 + newNextTime := timeutil.TimeStamp(nextTime + n*interval) + return n, newNextTime +} diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 38b6b5384..4b84cdd0c 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -18,13 +18,12 @@ type PointOperator struct { } func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) bool { - realAmount, err := limiter.CheckLimitWithFillUp(ctx.SourceType, models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount) + realAmount, err := limiter.CheckLimitWithFillUp(ctx.SourceType.Name(), models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount) if err != nil { return true } if realAmount < ctx.Reward.Amount { ctx.Remark = models.AppendRemark(ctx.Remark, fmt.Sprintf(LossMsg, ctx.Reward.Amount, realAmount)) - ctx.Reward.Amount = realAmount } return false diff --git a/services/task/task.go b/services/task/task.go index cd6ca830e..4c85ce52e 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -51,7 +51,7 @@ func accomplish(userId int64, taskType string) error { } //reward - reward.Send(&models.RewardOperateContext{ + reward.Operate(&models.RewardOperateContext{ SourceType: models.SourceTypeAccomplishTask, SourceId: logId, Reward: models.Reward{ From 7a3cc57f9f76d1c620a3816872043b32b7e2a6f7 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 24 Jun 2022 14:54:46 +0800 Subject: [PATCH 014/283] #2225 update --- models/action.go | 10 ++ models/attachment.go | 6 +- models/error.go | 12 ++ models/limit_config.go | 8 + models/models.go | 1 + models/repo_watch.go | 5 +- models/reward_admin_log.go | 46 ++++++ models/reward_operate_record.go | 84 ++++++++++- models/reward_periodic_task.go | 11 +- models/task_accomplish_log.go | 11 +- models/task_config.go | 30 ---- modules/auth/wechat/event_handle.go | 7 +- modules/cloudbrain/resty.go | 8 +- modules/cron/tasks_basic.go | 2 +- modules/notification/action/action.go | 103 +++++++++++++ modules/notification/base/notifier.go | 7 +- modules/notification/base/null.go | 8 +- modules/notification/notification.go | 15 +- modules/notification/task/task.go | 157 -------------------- modules/redis/redis_client/client.go | 4 +- modules/redis/redis_key/reward_redis_key.go | 5 +- modules/redis/redis_key/serial_redis_key.go | 10 ++ modules/setting/setting.go | 10 +- routers/repo/cloudbrain.go | 12 +- routers/repo/modelarts.go | 8 +- routers/reward/point/point.go | 46 ++++++ routers/routes/routes.go | 4 + routers/task/task.go | 15 ++ routers/user/setting/profile.go | 2 +- services/reward/admin_operate.go | 50 +++++++ services/reward/cloubrain_deduct.go | 17 ++- services/reward/limiter/limiter.go | 39 ++--- services/reward/operator.go | 54 +++++-- services/reward/period_task.go | 8 +- services/reward/point/point_operate.go | 2 +- services/reward/record.go | 20 +++ services/reward/serial.go | 21 +++ services/task/task.go | 31 +++- 38 files changed, 591 insertions(+), 298 deletions(-) create mode 100644 models/reward_admin_log.go delete mode 100644 modules/notification/task/task.go create mode 100644 modules/redis/redis_key/serial_redis_key.go create mode 100644 routers/task/task.go create mode 100644 services/reward/admin_operate.go create mode 100644 services/reward/record.go create mode 100644 services/reward/serial.go diff --git a/models/action.go b/models/action.go index 9b92b4192..456d5c6bc 100755 --- a/models/action.go +++ b/models/action.go @@ -58,6 +58,16 @@ const ( ActionCreateBenchMarkTask //29 ActionCreateNewModelTask //30 ActionCreateGPUTrainTask //31 + + ActionBindWechat //32issue_assignees + ActionCreateCloudbrainTask //33 + ActionDatasetRecommended //34 + ActionCreateImage //35 + ActionImageRecommend //36 + ActionChangeUserAvatar //37 + ActionPushCommits //38 + ActionForkRepo //39 + ) // Action represents user operation type and other information to diff --git a/models/attachment.go b/models/attachment.go index 0e4751ed2..453c819b1 100755 --- a/models/attachment.go +++ b/models/attachment.go @@ -654,9 +654,9 @@ func Attachments(opts *AttachmentsOptions) ([]*AttachmentInfo, int64, error) { return attachments, count, nil } -func GetAllUserIdByDatasetId(datasetId int64) ([]int64, error) { - r := make([]int64, 0) - if err := x.Table("attachment").Where("dataset_id = ?", datasetId).Distinct("uploader_id").Find(&r); err != nil { +func GetAllDatasetContributorByDatasetId(datasetId int64) ([]*User, error) { + r := make([]*User, 0) + if err := x.Select("distinct(user.*)").Table("attachment").Join("LEFT", "user", "user.ID = attachment.uploader_id").Where("attachment.dataset_id = ?", datasetId).Find(&r); err != nil { return nil, err } return r, nil diff --git a/models/error.go b/models/error.go index 19afa9d8b..7c7b0418b 100755 --- a/models/error.go +++ b/models/error.go @@ -2024,3 +2024,15 @@ func IsErrRecordNotExist(err error) bool { func (err ErrRecordNotExist) Error() string { return fmt.Sprintf("record not exist in database") } + +type ErrInsufficientPointsBalance struct { +} + +func IsErrInsufficientPointsBalance(err error) bool { + _, ok := err.(ErrInsufficientPointsBalance) + return ok +} + +func (err ErrInsufficientPointsBalance) Error() string { + return fmt.Sprintf("Insufficient points balance") +} diff --git a/models/limit_config.go b/models/limit_config.go index 62ff3bfbe..ce8d2cfc2 100644 --- a/models/limit_config.go +++ b/models/limit_config.go @@ -41,6 +41,14 @@ func (l LimitScope) Name() string { } } +type LimiterRejectPolicy string + +const ( + JustReject LimiterRejectPolicy = "JUST_REJECT" + PermittedOnce LimiterRejectPolicy = "PERMITTED_ONCE" + FillUp LimiterRejectPolicy = "FillUp" +) + type LimitConfig struct { ID int64 `xorm:"pk autoincr"` Tittle string diff --git a/models/models.go b/models/models.go index c6c0d6610..731b31960 100755 --- a/models/models.go +++ b/models/models.go @@ -151,6 +151,7 @@ func init() { new(RewardPeriodicTask), new(PointAccountLog), new(PointAccount), + new(RewardAdminLog), ) tablesStatistic = append(tablesStatistic, diff --git a/models/repo_watch.go b/models/repo_watch.go index 2d01bde1f..864aec254 100644 --- a/models/repo_watch.go +++ b/models/repo_watch.go @@ -25,6 +25,7 @@ const ( ) var ActionChan = make(chan *Action, 200) +var ActionChan4Task = make(chan Action, 200) // Watch is connection request for receiving repository notification. type Watch struct { @@ -199,6 +200,9 @@ func notifyWatchers(e Engine, actions ...*Action) error { if _, err = e.InsertOne(act); err != nil { return fmt.Errorf("insert new actioner: %v", err) } + // After InsertOne(act),the act has ID + // Send the act to task chan + ActionChan4Task <- *act if repoChanged { act.loadRepo() @@ -279,7 +283,6 @@ func notifyWatchers(e Engine, actions ...*Action) error { // NotifyWatchers creates batch of actions for every watcher. func NotifyWatchers(actions ...*Action) error { - error := notifyWatchers(x, actions...) producer(actions...) return error diff --git a/models/reward_admin_log.go b/models/reward_admin_log.go new file mode 100644 index 000000000..5e4258682 --- /dev/null +++ b/models/reward_admin_log.go @@ -0,0 +1,46 @@ +package models + +import ( + "code.gitea.io/gitea/modules/timeutil" +) + +const ( + RewardAdminLogProcessing = 1 + RewardAdminLogSuccess = 2 + RewardAdminLogFailed = 3 +) + +type RewardAdminLog struct { + ID int64 `xorm:"pk autoincr"` + LogId string `xorm:"INDEX NOT NULL"` + Amount int64 `xorm:"NOT NULL"` + RewardType string + Remark string + Status int + TargetUserId int64 `xorm:"INDEX NOT NULL"` + CreatorId int64 `xorm:"NOT NULL"` + CreatorName string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +func getRewardAdminLog(ra *RewardAdminLog) (*RewardAdminLog, error) { + has, err := x.Get(ra) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRecordNotExist{} + } + return ra, nil +} + +func InsertRewardAdminLog(ra *RewardAdminLog) (int64, error) { + return x.Insert(ra) +} + +func UpdateRewardAdminLogStatus(logId string, oldStatus, newStatus int) error { + _, err := x.Where("log_id = ? and status = ?", logId, oldStatus).Update(&RewardAdminLog{Status: newStatus}) + if err != nil { + return err + } + return nil +} diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index d3b2e0a10..d58accfa5 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -95,6 +95,7 @@ func GetRewardOperateTypeInstance(s string) RewardOperateType { const ( OperateTypeIncrease RewardOperateType = "INCREASE" OperateTypeDecrease RewardOperateType = "DECREASE" + OperateTypeNull RewardOperateType = "NIL" ) const ( @@ -105,11 +106,18 @@ const ( const Semicolon = ";" +type RewardOperateOrderBy string + +const ( + RewardOrderByID RewardOperateOrderBy = "id" +) + type RewardOperateRecord struct { ID int64 `xorm:"pk autoincr"` - RecordId string `xorm:"INDEX NOT NULL"` + SerialNo string `xorm:"INDEX NOT NULL"` UserId int64 `xorm:"INDEX NOT NULL"` Amount int64 `xorm:"NOT NULL"` + Tittle string RewardType string `xorm:"NOT NULL"` SourceType string `xorm:"NOT NULL"` SourceId string `xorm:"INDEX NOT NULL"` @@ -121,6 +129,32 @@ type RewardOperateRecord struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } +type AdminRewardOperateReq struct { + TargetUserId int64 `binding:"Required"` + OperateType RewardOperateType `binding:"Required"` + Amount int64 `binding:"Required;Range(1,100000)"` + Remark string + RewardType RewardType +} + +func (r RewardOperateRecord) ToShow() RewardOperateRecordShow { + return RewardOperateRecordShow{ + SerialNo: r.SerialNo, + Date: r.CreatedUnix, + Tittle: r.Tittle, + OperateType: r.OperateType, + Amount: r.Amount, + } +} + +type RewardOperateRecordShow struct { + SerialNo string + Date timeutil.TimeStamp + Tittle string + OperateType string + Amount int64 +} + func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error) { has, err := x.Get(tl) if err != nil { @@ -140,14 +174,14 @@ func GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId, operat return getPointOperateRecord(t) } -func GetPointOperateRecordByRecordId(recordId string) (*RewardOperateRecord, error) { +func GetPointOperateRecordBySerialNo(serialNo string) (*RewardOperateRecord, error) { t := &RewardOperateRecord{ - RecordId: recordId, + SerialNo: serialNo, } return getPointOperateRecord(t) } -func InsertAwardOperateRecord(tl *RewardOperateRecord) (int64, error) { +func InsertRewardOperateRecord(tl *RewardOperateRecord) (int64, error) { return x.Insert(tl) } @@ -175,11 +209,13 @@ func SumRewardAmountInTaskPeriod(rewardType string, sourceType string, userId in type RewardOperateContext struct { SourceType SourceType SourceId string + Tittle string Remark string Reward Reward TargetUserId int64 RequestId string OperateType RewardOperateType + RejectPolicy LimiterRejectPolicy } type Reward struct { @@ -202,3 +238,43 @@ type UserRewardOperation struct { func AppendRemark(remark, appendStr string) string { return strings.TrimPrefix(remark+Semicolon+appendStr, Semicolon) } + +type RewardRecordListOpts struct { + ListOptions + UserId int64 + OperateType RewardOperateType + RewardType RewardType + OrderBy RewardOperateOrderBy +} + +func GetRewardRecordList(opts RewardRecordListOpts) ([]RewardOperateRecord, int64, error) { + if opts.Page <= 0 { + opts.Page = 1 + } + + if len(opts.OrderBy) == 0 { + opts.OrderBy = RewardOrderByID + } + + r := make([]RewardOperateRecord, 0) + cond := builder.NewCond() + if opts.UserId > 0 { + cond = cond.And(builder.Eq{"user_id": opts.UserId}) + } + if opts.OperateType != OperateTypeNull { + cond = cond.And(builder.Eq{"operate_type": opts.OperateType.Name()}) + } + cond = cond.And(builder.Eq{"reward_type": opts.RewardType.Name()}) + cond = cond.And(builder.Gt{"amount": 0}) + + count, err := x.Where(cond).Count(&RewardOperateRecord{}) + if err != nil { + return nil, 0, err + } + + err = x.Where(cond).Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).OrderBy(string(opts.OrderBy)).Find(&r) + if err != nil { + return nil, 0, err + } + return r, count, nil +} diff --git a/models/reward_periodic_task.go b/models/reward_periodic_task.go index e6ebd17c2..5db5301b5 100644 --- a/models/reward_periodic_task.go +++ b/models/reward_periodic_task.go @@ -29,7 +29,7 @@ func (r PeriodType) Name() string { type RewardPeriodicTask struct { ID int64 `xorm:"pk autoincr"` - OperateRecordId string `xorm:"INDEX NOT NULL"` + OperateSerialNo string `xorm:"INDEX NOT NULL"` DelaySeconds int64 IntervalSeconds int64 Amount int64 `xorm:"NOT NULL"` @@ -45,6 +45,7 @@ type StartPeriodicTaskOpts struct { SourceType SourceType SourceId string Remark string + Tittle string TargetUserId int64 RequestId string OperateType RewardOperateType @@ -76,7 +77,7 @@ func IncrRewardTaskSuccessCount(t RewardPeriodicTask, count int64, nextTime time sess.Rollback() return err } - _, err = sess.Exec("update reward_operate_record set amount = amount + ? ,updated_unix = ? where record_id = ?", count*t.Amount, timeutil.TimeStampNow(), t.OperateRecordId) + _, err = sess.Exec("update reward_operate_record set amount = amount + ? ,updated_unix = ? where serial_no = ?", count*t.Amount, timeutil.TimeStampNow(), t.OperateSerialNo) if err != nil { sess.Rollback() return err @@ -88,7 +89,7 @@ func IncrRewardTaskSuccessCount(t RewardPeriodicTask, count int64, nextTime time func GetPeriodicTaskBySourceIdAndType(sourceType SourceType, sourceId string, operateType RewardOperateType) (*RewardPeriodicTask, error) { r := RewardPeriodicTask{} _, err := x.SQL("select rpt.* from reward_periodic_task rpt "+ - "inner join reward_operate_record ror on rpt.operate_record_id = ror.record_id"+ + "inner join reward_operate_record ror on rpt.operate_serial_no = ror.serial_no"+ " where ror.source_type = ? and source_id = ? and operate_type = ? ", sourceType.Name(), sourceId, operateType.Name()).Get(&r) if err != nil { return nil, err @@ -96,7 +97,7 @@ func GetPeriodicTaskBySourceIdAndType(sourceType SourceType, sourceId string, op return &r, nil } -func StopPeriodicTask(taskId int64, operateRecordId string, stopTime time.Time) error { +func StopPeriodicTask(taskId int64, operateSerialNo string, stopTime time.Time) error { sess := x.NewSession() defer sess.Close() _, err := sess.Where("id = ? and status = ?", taskId, PeriodicTaskStatusRunning).Update(&RewardPeriodicTask{Status: PeriodicTaskStatusFinished, FinishedUnix: timeutil.TimeStamp(stopTime.Unix())}) @@ -104,7 +105,7 @@ func StopPeriodicTask(taskId int64, operateRecordId string, stopTime time.Time) sess.Rollback() return err } - _, err = sess.Where("record_id = ? and status = ?", operateRecordId, OperateStatusOperating).Update(&RewardOperateRecord{Status: OperateStatusSucceeded}) + _, err = sess.Where("serial_no = ? and status = ?", operateSerialNo, OperateStatusOperating).Update(&RewardOperateRecord{Status: OperateStatusSucceeded}) if err != nil { sess.Rollback() return err diff --git a/models/task_accomplish_log.go b/models/task_accomplish_log.go index 3736d1c41..a1edb71ee 100644 --- a/models/task_accomplish_log.go +++ b/models/task_accomplish_log.go @@ -6,11 +6,12 @@ import ( ) type TaskAccomplishLog struct { - ID int64 `xorm:"pk autoincr"` - LogId string `xorm:"INDEX NOT NULL"` - ConfigId int64 `xorm:"NOT NULL"` - TaskCode string `xorm:"NOT NULL"` - UserId int64 `xorm:"INDEX NOT NULL"` + ID int64 `xorm:"pk autoincr"` + LogId string `xorm:"INDEX NOT NULL"` + ConfigId int64 `xorm:"NOT NULL"` + TaskCode string `xorm:"NOT NULL"` + UserId int64 `xorm:"INDEX NOT NULL"` + ActionId int64 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` } diff --git a/models/task_config.go b/models/task_config.go index cd4329834..eef3a6c33 100644 --- a/models/task_config.go +++ b/models/task_config.go @@ -4,36 +4,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" ) -const ( - TaskTypeNewIssue = "NEW_ISSUE" - TaskTypeIssueChangeStatus = "ISSUE_CHANGE_STATUS" - TaskTypeCreateIssueComment = "CREATE_ISSUE_COMMENT" - TaskTypeNewPullRequest = "NEW_PULL_REQUEST" - TaskTypeRenameRepository = "RENAME_REPOSITORY" - TaskTypeAliasRepository = "ALIAS_REPOSITORY" - TaskTypeTransferRepository = "TRANSFER_REPOSITORY" - TaskTypeCreateRepository = "CREATE_REPOSITORY" - TaskTypeCreatePublicRepository = "CREATE_PUBLIC_REPOSITORY" - TaskTypeForkRepository = "FORK_REPOSITORY" - TaskTypePullRequestReview = "PULL_REQUEST_REVIEW" - TaskTypeCommentPull = "COMMENT_PULL" - TaskTypeApprovePullRequest = "APPROVE_PULL_REQUEST" - TaskTypeRejectPullRequest = "REJECT_PULL_REQUEST" - TaskTypeMergePullRequest = "MERGE_PULL_REQUEST" - TaskTypeSyncPushCommits = "SYNC_PUSH_COMMITS" - TaskTypeSyncCreateRef = "SYNC_CREATE_REF" - TaskTypeSyncDeleteRef = "SYNC_DELETE_REF" - TaskTypeBindWechat = "BIND_WECHAT" - TaskTypeUploadAttachment = "UPLOAD_ATTACHMENT" - TaskTypeCreateCloudbrainTask = "CREATE_CLOUDBRAIN_TASK" - TaskTypeDatasetRecommended = "DATASET_RECOMMENDED" - TaskTypeCreateModel = "CREATE_MODEL" - TaskTypeCreatePublicImage = "CREATE_PUBLIC_IMAGE" - TaskTypeImageRecommend = "IMAGE_RECOMMEND" - TaskTypeChangeUserAvatar = "CHANGE_USER_AVATAR" - TaskTypePushCommits = "PUSH_COMMITS" -) - const ( PeriodNotCycle = "NOT_CYCLE" PeriodDaily = "DAILY" diff --git a/modules/auth/wechat/event_handle.go b/modules/auth/wechat/event_handle.go index 67c3a7265..399537f1e 100644 --- a/modules/auth/wechat/event_handle.go +++ b/modules/auth/wechat/event_handle.go @@ -1,6 +1,7 @@ package wechat import ( + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" @@ -72,6 +73,10 @@ func HandleSubscribeEvent(we WechatEvent) string { jsonStr, _ := json.Marshal(qrCache) redis_client.Setex(redis_key.WechatBindingUserIdKey(sceneStr), string(jsonStr), 60*time.Second) } - notification.NotifyWechatBind(qrCache.UserId, we.FromUserName) + u, err := models.GetUserByID(qrCache.UserId) + if err == nil { + notification.NotifyWechatBind(u, we.FromUserName) + } + return BIND_REPLY_SUCCESS } diff --git a/modules/cloudbrain/resty.go b/modules/cloudbrain/resty.go index 75614e571..d45468ddb 100755 --- a/modules/cloudbrain/resty.go +++ b/modules/cloudbrain/resty.go @@ -212,7 +212,7 @@ func getQueryString(page int, size int, name string) string { return fmt.Sprintf("pageIndex=%d&pageSize=%d&name=%s", page, size, name) } -func CommitImage(jobID string, params models.CommitImageParams) error { +func CommitImage(jobID string, params models.CommitImageParams, doer *models.User) error { imageTag := strings.TrimSpace(params.ImageTag) dbImage, err := models.GetImageByTag(imageTag) @@ -314,12 +314,12 @@ sendjob: }) if err == nil { go updateImageStatus(image, isSetCreatedUnix, createTime) - notification.NotifyCreateImage(params.UID, image) + notification.NotifyCreateImage(doer, image) } return err } -func CommitAdminImage(params models.CommitImageParams) error { +func CommitAdminImage(params models.CommitImageParams, doer *models.User) error { imageTag := strings.TrimSpace(params.ImageTag) exist, err := models.IsImageExist(imageTag) @@ -357,7 +357,7 @@ func CommitAdminImage(params models.CommitImageParams) error { return nil }) if err == nil { - notification.NotifyCreateImage(params.UID, image) + notification.NotifyCreateImage(doer, image) } return err } diff --git a/modules/cron/tasks_basic.go b/modules/cron/tasks_basic.go index 39100594d..5892699eb 100755 --- a/modules/cron/tasks_basic.go +++ b/modules/cron/tasks_basic.go @@ -212,7 +212,7 @@ func registerRewardPeriodTask() { RegisterTaskFatal("reward_period_task", &BaseConfig{ Enabled: true, RunAtStart: true, - Schedule: "@every 5m", + Schedule: "@every 1m", }, func(ctx context.Context, _ *models.User, _ Config) error { reward.StartRewardTask() return nil diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index 2ac73c2c3..6a43c6e9a 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -5,6 +5,7 @@ package action import ( + "code.gitea.io/gitea/modules/auth" "encoding/json" "fmt" "path" @@ -345,3 +346,105 @@ func (a *actionNotifier) NotifyOtherTask(doer *models.User, repo *models.Reposit log.Error("notifyWatchers: %v", err) } } + +func (t *actionNotifier) NotifyWechatBind(user *models.User, wechatOpenId string) { + act := &models.Action{ + ActUserID: user.ID, + ActUser: user, + OpType: models.ActionBindWechat, + IsPrivate: true, + Content: wechatOpenId, + } + if err := models.NotifyWatchers(act); err != nil { + log.Error("notifyWatchers: %v", err) + } +} + +func (t *actionNotifier) NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) { + switch action { + case "recommend": + users, err := models.GetAllDatasetContributorByDatasetId(dataset.ID) + if err != nil { + return + } + var actions = make([]*models.Action, 0) + for _, user := range users { + actions = append(actions, &models.Action{ + OpType: models.ActionDatasetRecommended, + ActUserID: user.ID, + ActUser: user, + RepoID: dataset.RepoID, + Repo: dataset.Repo, + Content: fmt.Sprint(dataset.ID), + }) + } + if err := models.NotifyWatchers(actions...); err != nil { + log.Error("notifyWatchers: %v", err) + } + } +} + +func (t *actionNotifier) NotifyCreateImage(doer *models.User, image models.Image) { + act := &models.Action{ + ActUserID: doer.ID, + ActUser: doer, + OpType: models.ActionCreateImage, + IsPrivate: image.IsPrivate, + Content: fmt.Sprint(image.ID), + } + if err := models.NotifyWatchers(act); err != nil { + log.Error("notifyWatchers: %v", err) + } +} + +func (t *actionNotifier) NotifyImageRecommend(optUser *models.User, imageId int64, action string) { + image, err := models.GetImageByID(imageId) + if err != nil { + return + } + u, err := models.GetUserByID(image.UID) + if err != nil { + return + } + switch action { + case "recommend": + act := &models.Action{ + ActUserID: u.ID, + ActUser: u, + OpType: models.ActionImageRecommend, + IsPrivate: false, + Content: fmt.Sprint(imageId), + } + if err := models.NotifyWatchers(act); err != nil { + log.Error("notifyWatchers: %v", err) + } + } +} + +func (t *actionNotifier) NotifyChangeUserAvatar(user *models.User, form auth.AvatarForm) { + act := &models.Action{ + ActUserID: user.ID, + ActUser: user, + OpType: models.ActionChangeUserAvatar, + IsPrivate: true, + } + if err := models.NotifyWatchers(act); err != nil { + log.Error("notifyWatchers: %v", err) + } +} + +func (t *actionNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) { + act := &models.Action{ + ActUserID: pusher.ID, + ActUser: pusher, + OpType: models.ActionPushCommits, + RepoID: repo.ID, + Repo: repo, + RefName: refName, + IsPrivate: repo.IsPrivate, + Content: fmt.Sprintf("%s|%s", oldCommitID, newCommitID), + } + if err := models.NotifyWatchers(act); err != nil { + log.Error("notifyWatchers: %v", err) + } +} diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index c3c7f404a..7673a5909 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -6,6 +6,7 @@ package base import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/repository" ) @@ -56,9 +57,9 @@ type Notifier interface { NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) - NotifyWechatBind(userId int64, wechatOpenId string) + NotifyWechatBind(user *models.User, wechatOpenId string) NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) - NotifyCreateImage(optUserId int64, image models.Image) + NotifyCreateImage(doer *models.User, image models.Image) NotifyImageRecommend(optUser *models.User, imageId int64, action string) - NotifyChangeUserAvatar(user *models.User) + NotifyChangeUserAvatar(user *models.User, form auth.AvatarForm) } diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index c0a224697..eea5c5e77 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -6,6 +6,7 @@ package base import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/repository" ) @@ -159,18 +160,19 @@ func (*NullNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, } -func (*NullNotifier) NotifyWechatBind(userId int64, wechatOpenId string) { +func (*NullNotifier) NotifyWechatBind(user *models.User, wechatOpenId string) { } func (*NullNotifier) NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) { } -func (*NullNotifier) NotifyCreateImage(optUserId int64, image models.Image) { +func (*NullNotifier) NotifyCreateImage(doer *models.User, image models.Image) { } func (*NullNotifier) NotifyImageRecommend(optUser *models.User, imageId int64, action string) { } -func (*NullNotifier) NotifyChangeUserAvatar(user *models.User) { +func (*NullNotifier) NotifyChangeUserAvatar(user *models.User, form auth.AvatarForm) { + } diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 118bdf994..d652dc043 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -6,11 +6,11 @@ package notification import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/notification/action" "code.gitea.io/gitea/modules/notification/base" "code.gitea.io/gitea/modules/notification/indexer" "code.gitea.io/gitea/modules/notification/mail" - "code.gitea.io/gitea/modules/notification/task" "code.gitea.io/gitea/modules/notification/ui" "code.gitea.io/gitea/modules/notification/webhook" "code.gitea.io/gitea/modules/repository" @@ -36,7 +36,6 @@ func NewContext() { RegisterNotifier(indexer.NewNotifier()) RegisterNotifier(webhook.NewNotifier()) RegisterNotifier(action.NewNotifier()) - RegisterNotifier(task.NewNotifier()) } // NotifyUploadAttachment notifies attachment upload message to notifiers @@ -273,9 +272,9 @@ func NotifySyncDeleteRef(pusher *models.User, repo *models.Repository, refType, } // NotifyWechatBind notifies wechat bind -func NotifyWechatBind(userId int64, wechatOpenId string) { +func NotifyWechatBind(user *models.User, wechatOpenId string) { for _, notifier := range notifiers { - notifier.NotifyWechatBind(userId, wechatOpenId) + notifier.NotifyWechatBind(user, wechatOpenId) } } @@ -287,9 +286,9 @@ func NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, actio } // NotifyDatasetRecommend -func NotifyCreateImage(optUserId int64, image models.Image) { +func NotifyCreateImage(doer *models.User, image models.Image) { for _, notifier := range notifiers { - notifier.NotifyCreateImage(optUserId, image) + notifier.NotifyCreateImage(doer, image) } } @@ -301,8 +300,8 @@ func NotifyImageRecommend(optUser *models.User, imageId int64, action string) { } // NotifyDatasetRecommend -func NotifyChangeUserAvatar(user *models.User) { +func NotifyChangeUserAvatar(user *models.User, form auth.AvatarForm) { for _, notifier := range notifiers { - notifier.NotifyChangeUserAvatar(user) + notifier.NotifyChangeUserAvatar(user, form) } } diff --git a/modules/notification/task/task.go b/modules/notification/task/task.go deleted file mode 100644 index 077d6699b..000000000 --- a/modules/notification/task/task.go +++ /dev/null @@ -1,157 +0,0 @@ -package task - -import ( - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/notification/base" - "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/services/task" - "strings" -) - -type taskNotifier struct { - base.NullNotifier -} - -var ( - _ base.Notifier = &taskNotifier{} -) - -// NewNotifier create a new actionNotifier notifier -func NewNotifier() base.Notifier { - return &taskNotifier{} -} - -func (t *taskNotifier) NotifyNewIssue(issue *models.Issue) { - task.Accomplish(issue.Poster.ID, models.TaskTypeNewIssue) -} - -// NotifyIssueChangeStatus notifies close or reopen issue to notifiers -func (t *taskNotifier) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) { - task.Accomplish(doer.ID, models.TaskTypeIssueChangeStatus) -} - -// NotifyCreateIssueComment notifies comment on an issue to notifiers -func (t *taskNotifier) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, - issue *models.Issue, comment *models.Comment) { - task.Accomplish(doer.ID, models.TaskTypeCreateIssueComment) -} - -func (t *taskNotifier) NotifyNewPullRequest(pull *models.PullRequest) { - task.Accomplish(pull.Issue.Poster.ID, models.TaskTypeNewPullRequest) -} - -func (t *taskNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldRepoName string) { - task.Accomplish(doer.ID, models.TaskTypeRenameRepository) -} - -func (t *taskNotifier) NotifyAliasRepository(doer *models.User, repo *models.Repository, oldAlias string) { - task.Accomplish(doer.ID, models.TaskTypeAliasRepository) -} - -func (t *taskNotifier) NotifyTransferRepository(doer *models.User, repo *models.Repository, oldOwnerName string) { - task.Accomplish(doer.ID, models.TaskTypeTransferRepository) -} - -func (t *taskNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { - if !repo.IsPrivate { - task.Accomplish(doer.ID, models.TaskTypeCreatePublicRepository) - } - -} - -func (t *taskNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { - task.Accomplish(doer.ID, models.TaskTypeForkRepository) -} - -func (t *taskNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment) { - for _, lines := range review.CodeComments { - for _, comments := range lines { - for _, _ = range comments { - task.Accomplish(review.Reviewer.ID, models.TaskTypePullRequestReview) - } - } - } - if review.Type != models.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" { - - switch review.Type { - case models.ReviewTypeApprove: - task.Accomplish(review.Reviewer.ID, models.TaskTypeApprovePullRequest) - case models.ReviewTypeReject: - task.Accomplish(review.Reviewer.ID, models.TaskTypeRejectPullRequest) - default: - task.Accomplish(review.Reviewer.ID, models.TaskTypeCommentPull) - } - - } -} - -func (t *taskNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User) { - task.Accomplish(doer.ID, models.TaskTypeMergePullRequest) -} - -func (t *taskNotifier) NotifySyncPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) { - task.Accomplish(repo.OwnerID, models.TaskTypeSyncPushCommits) -} - -func (t *taskNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) { - task.Accomplish(repo.OwnerID, models.TaskTypeSyncCreateRef) -} - -func (t *taskNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) { - task.Accomplish(repo.OwnerID, models.TaskTypeSyncDeleteRef) -} - -func (t *taskNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) { - switch optype { - case models.ActionUploadAttachment: - task.Accomplish(doer.ID, models.TaskTypeUploadAttachment) - case models.ActionCreateDebugGPUTask, - models.ActionCreateDebugNPUTask, - models.ActionCreateTrainTask, - models.ActionCreateInferenceTask, - models.ActionCreateBenchMarkTask, - models.ActionCreateGPUTrainTask: - task.Accomplish(doer.ID, models.TaskTypeCreateCloudbrainTask) - case models.ActionCreateNewModelTask: - task.Accomplish(doer.ID, models.TaskTypeCreateModel) - } - return -} - -func (t *taskNotifier) NotifyWechatBind(userId int64, wechatOpenId string) { - task.Accomplish(userId, models.TaskTypeBindWechat) -} - -func (t *taskNotifier) NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) { - switch action { - case "recommend": - userIds, err := models.GetAllUserIdByDatasetId(dataset.ID) - if err != nil { - return - } - for _, userId := range userIds { - task.Accomplish(userId, models.TaskTypeDatasetRecommended) - } - } -} - -func (t *taskNotifier) NotifyCreateImage(optUserId int64, image models.Image) { - if !image.IsPrivate { - task.Accomplish(optUserId, models.TaskTypeCreatePublicImage) - } -} - -func (t *taskNotifier) NotifyImageRecommend(optUser *models.User, imageId int64, action string) { - switch action { - case "recommend": - task.Accomplish(optUser.ID, models.TaskTypeImageRecommend) - } -} - -func (t *taskNotifier) NotifyChangeUserAvatar(user *models.User) { - task.Accomplish(user.ID, models.TaskTypeChangeUserAvatar) -} - -func (t *taskNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) { - task.Accomplish(pusher.ID, models.TaskTypePushCommits) -} diff --git a/modules/redis/redis_client/client.go b/modules/redis/redis_client/client.go index c5cb936b3..e795234df 100644 --- a/modules/redis/redis_client/client.go +++ b/modules/redis/redis_client/client.go @@ -99,11 +99,11 @@ func IncrBy(key string, n int64) (int64, error) { } -func Expire(key string, expireSeconds int64) error { +func Expire(key string, expireTime time.Duration) error { redisClient := labelmsg.Get() defer redisClient.Close() - _, err := redisClient.Do("EXPIRE", key, expireSeconds) + _, err := redisClient.Do("EXPIRE", key, int64(expireTime.Seconds())) if err != nil { return err } diff --git a/modules/redis/redis_key/reward_redis_key.go b/modules/redis/redis_key/reward_redis_key.go index f6c9480a9..05c10ce4f 100644 --- a/modules/redis/redis_key/reward_redis_key.go +++ b/modules/redis/redis_key/reward_redis_key.go @@ -1,6 +1,8 @@ package redis_key -import "fmt" +import ( + "fmt" +) const REWARD_REDIS_PREFIX = "reward" @@ -11,6 +13,7 @@ func RewardOperateLock(requestId string, sourceType string, operateType string) func RewardOperateNotification() string { return KeyJoin(REWARD_REDIS_PREFIX, "operate", "notification") } + func RewardTaskRunningLock(taskId int64) string { return KeyJoin(REWARD_REDIS_PREFIX, "periodic_task", fmt.Sprint(taskId), "lock") } diff --git a/modules/redis/redis_key/serial_redis_key.go b/modules/redis/redis_key/serial_redis_key.go new file mode 100644 index 000000000..c0ecf39eb --- /dev/null +++ b/modules/redis/redis_key/serial_redis_key.go @@ -0,0 +1,10 @@ +package redis_key + +import "time" + +const SERIAL_REDIS_PREFIX = "serial" + +func RewardSerialCounter(now time.Time) string { + h := now.Format("200601021504") + return KeyJoin(SERIAL_REDIS_PREFIX, "reward_operate", h, "counter") +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index b5ffe6eab..217388789 100755 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -232,7 +232,7 @@ var ( TimeoutStep: 10 * time.Second, MaxTimeout: 60 * time.Second, EventSourceUpdateTime: 10 * time.Second, - RewardNotifyUpdateTime: 3 * time.Second, + RewardNotifyUpdateTime: 2 * time.Second, }, Admin: struct { UserPagingNum int @@ -549,7 +549,9 @@ var ( WechatAuthSwitch bool //point config - CloudBrainTaskPointPaySwitch bool + CloudBrainPaySwitch bool + CloudBrainPayDelay time.Duration + CloudBrainPayInterval time.Duration //nginx proxy PROXYURL string @@ -1380,7 +1382,9 @@ func NewContext() { WechatAuthSwitch = sec.Key("AUTH_SWITCH").MustBool(false) sec = Cfg.Section("point") - CloudBrainTaskPointPaySwitch = sec.Key("CLOUDBRAIN_PAY_SWITCH").MustBool(false) + CloudBrainPaySwitch = sec.Key("CLOUDBRAIN_PAY_SWITCH").MustBool(false) + CloudBrainPayDelay = sec.Key("CLOUDBRAIN_PAY_DELAY").MustDuration(30 * time.Minute) + CloudBrainPayInterval = sec.Key("CLOUDBRAIN_PAY_INTERVAL").MustDuration(60 * time.Minute) SetRadarMapConfig() diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index b4d532ab0..bcbc5ea6d 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -233,7 +233,7 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { if !reward.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) cloudBrainNewDataPrepare(ctx) - ctx.RenderWithErr("point balance not enough", tpl, &form) + ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tpl, &form) return } @@ -319,7 +319,7 @@ func CloudBrainRestart(ctx *context.Context) { if !reward.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) resultCode = "-1" - errorMsg = "insufficient points balance" + errorMsg = models.ErrInsufficientPointsBalance{}.Error() break } @@ -737,7 +737,7 @@ func CloudBrainAdminCommitImage(ctx *context.Context, form auth.CommitAdminImage UID: ctx.User.ID, Type: models.GetRecommondType(form.IsRecommend), Place: form.Place, - }) + }, ctx.User) if err != nil { log.Error("CommitImagefailed") if models.IsErrImageTagExist(err) { @@ -784,7 +784,7 @@ func CloudBrainCommitImage(ctx *context.Context, form auth.CommitImageCloudBrain CloudBrainType: form.Type, Topics: validTopics, UID: ctx.User.ID, - }) + }, ctx.User) if err != nil { log.Error("CommitImage(%s) failed:%v", ctx.Cloudbrain.JobName, err.Error(), ctx.Data["msgID"]) if models.IsErrImageTagExist(err) { @@ -1862,7 +1862,7 @@ func BenchMarkAlgorithmCreate(ctx *context.Context, form auth.CreateCloudBrainFo if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) cloudBrainNewDataPrepare(ctx) - ctx.RenderWithErr("point balance not enough", tplCloudBrainBenchmarkNew, &form) + ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplCloudBrainBenchmarkNew, &form) return } @@ -2024,7 +2024,7 @@ func ModelBenchmarkCreate(ctx *context.Context, form auth.CreateCloudBrainForm) if !reward.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) cloudBrainNewDataPrepare(ctx) - ctx.RenderWithErr("point balance not enough", tpl, &form) + ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tpl, &form) return } diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index dea996a50..6e5175e15 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -210,7 +210,7 @@ func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeDebug), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) cloudBrainNewDataPrepare(ctx) - ctx.RenderWithErr("point balance not enough", tplModelArtsNotebookNew, &form) + ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsNotebookNew, &form) return } count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID) @@ -429,7 +429,7 @@ func NotebookManage(ctx *context.Context) { if !reward.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) resultCode = "-1" - errorMsg = "point balance not enough" + errorMsg = models.ErrInsufficientPointsBalance{}.Error() break return } @@ -1005,7 +1005,7 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeTrain), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) cloudBrainNewDataPrepare(ctx) - ctx.RenderWithErr("point balance not enough", tplModelArtsTrainJobNew, &form) + ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsTrainJobNew, &form) return } count, err := models.GetCloudbrainTrainJobCountByUserID(ctx.User.ID) @@ -1854,7 +1854,7 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeInference), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) inferenceJobErrorNewDataPrepare(ctx, form) - ctx.RenderWithErr("point balance not enough", tplModelArtsInferenceJobNew, &form) + ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsInferenceJobNew, &form) return } count, err := models.GetCloudbrainInferenceJobCountByUserID(ctx.User.ID) diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index eaae76c4f..3140b4c38 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -1,8 +1,10 @@ package point import ( + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/routers/response" + "code.gitea.io/gitea/services/reward" "code.gitea.io/gitea/services/reward/point/account" "net/http" ) @@ -29,3 +31,47 @@ func GetPointAccount(ctx *context.Context) { } ctx.JSON(http.StatusOK, response.SuccessWithData(res)) } + +func GetPointRecordList(ctx *context.Context) { + operateType := ctx.Query("operate") + page := ctx.QueryInt("page") + var orderBy models.RewardOperateOrderBy + switch ctx.Query("sort") { + default: + orderBy = models.RewardOrderByID + } + t := models.GetRewardOperateTypeInstance(operateType) + if t == "" { + ctx.JSON(http.StatusOK, response.ServerError("param error")) + return + } + + r, err := reward.GetRewardRecordList(models.RewardRecordListOpts{ + ListOptions: models.ListOptions{PageSize: 20, Page: page}, + UserId: ctx.User.ID, + OperateType: t, + RewardType: models.RewardTypePoint, + OrderBy: orderBy, + }) + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + + ctx.JSON(http.StatusOK, response.SuccessWithData(r)) + return +} + +func OperatePointAccountBalance(ctx *context.Context, req models.AdminRewardOperateReq) { + req.RewardType = models.RewardTypePoint + if req.OperateType.Name() == "" { + ctx.JSON(http.StatusOK, "param error") + return + } + err := reward.AdminBalanceOperate(req, ctx.User) + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + ctx.JSON(http.StatusOK, response.Success()) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 3ce633f93..0658765ca 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -324,6 +324,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/", routers.Home) m.Get("/dashboard", routers.Dashboard) go routers.SocketManager.Run() + go task.RunTask() m.Get("/action/notification", routers.ActionNotification) m.Get("/reward/notification", routers.ActionNotification) m.Get("/recommend/org", routers.RecommendOrgFromPromote) @@ -594,6 +595,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/reward/point", func() { m.Get("/limiter/list", point.GetPointLimitConfigList) m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) + m.Post("/operate", binding.Bind(models.AdminRewardOperateReq{}), point.OperatePointAccountBalance) }) m.Group("/task/config", func() { @@ -601,6 +603,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/add", bindIgnErr(models.TaskConfigWithLimit{}), task.AddTaskConfig) m.Post("/add/batch", bindIgnErr(models.BatchLimitConfigVO{}), task.BatchAddTaskConfig) }) + }, adminReq) // ***** END: Admin ***** @@ -1330,6 +1333,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/reward/point", func() { m.Get("/account", point.GetPointAccount) + m.Get("/record/list", point.GetPointRecordList) }, reqSignIn) if setting.API.EnableSwagger { diff --git a/routers/task/task.go b/routers/task/task.go new file mode 100644 index 000000000..1d3b8595b --- /dev/null +++ b/routers/task/task.go @@ -0,0 +1,15 @@ +package task + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/services/task" +) + +func RunTask() { + for { + select { + case action := <-models.ActionChan4Task: + task.Accomplish(action) + } + } +} diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go index 1c1e664d0..0d788b422 100755 --- a/routers/user/setting/profile.go +++ b/routers/user/setting/profile.go @@ -166,7 +166,7 @@ func AvatarPost(ctx *context.Context, form auth.AvatarForm) { if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil { ctx.Flash.Error(err.Error()) } else { - notification.NotifyChangeUserAvatar(ctx.User) + notification.NotifyChangeUserAvatar(ctx.User, form) ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) } diff --git a/services/reward/admin_operate.go b/services/reward/admin_operate.go new file mode 100644 index 000000000..1eec0f414 --- /dev/null +++ b/services/reward/admin_operate.go @@ -0,0 +1,50 @@ +package reward + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/reward/limiter" +) + +func AdminBalanceOperate(req models.AdminRewardOperateReq, doer *models.User) error { + logId := util.UUID() + _, err := models.InsertRewardAdminLog(&models.RewardAdminLog{ + LogId: logId, + Amount: req.Amount, + RewardType: req.RewardType.Name(), + TargetUserId: req.TargetUserId, + CreatorId: doer.ID, + CreatorName: doer.Name, + Remark: req.Remark, + Status: models.RewardAdminLogProcessing, + }) + if err != nil { + log.Error("AdminBalanceOperate InsertRewardAdminLog error.%v", err) + return err + } + + //reward + err = Operate(&models.RewardOperateContext{ + SourceType: models.SourceTypeAdminOperate, + SourceId: logId, + Tittle: "管理员操作", + Reward: models.Reward{ + Amount: req.Amount, + Type: req.RewardType, + }, + TargetUserId: req.TargetUserId, + RequestId: logId, + OperateType: req.OperateType, + Remark: req.Remark, + RejectPolicy: limiter.JustReject, + }) + + if err != nil { + log.Error("AdminBalanceOperate operate error.%v", err) + models.UpdateRewardAdminLogStatus(logId, models.RewardAdminLogProcessing, models.RewardAdminLogFailed) + return err + } + models.UpdateRewardAdminLogStatus(logId, models.RewardAdminLogProcessing, models.RewardAdminLogSuccess) + return nil +} diff --git a/services/reward/cloubrain_deduct.go b/services/reward/cloubrain_deduct.go index 61068a87a..ce23e2dc7 100644 --- a/services/reward/cloubrain_deduct.go +++ b/services/reward/cloubrain_deduct.go @@ -15,9 +15,11 @@ var ( TrainResourceSpecs *models.ResourceSpecs ) +const RUN_CLOUDBRAIN_TASK_TITTLE = "运行云脑任务" + //IsPointBalanceEnough check whether the user's point balance is bigger than task unit price func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int) bool { - if !setting.CloudBrainTaskPointPaySwitch { + if !setting.CloudBrainPaySwitch { return true } spec := getResourceSpec(jobType, resourceSpecId) @@ -33,7 +35,7 @@ func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int } func StartCloudBrainPointDeductTask(task models.Cloudbrain) { - if !setting.CloudBrainTaskPointPaySwitch { + if !setting.CloudBrainPaySwitch { return } @@ -48,11 +50,12 @@ func StartCloudBrainPointDeductTask(task models.Cloudbrain) { TargetUserId: task.UserID, RequestId: getCloudBrainPointTaskSourceId(task), OperateType: models.OperateTypeDecrease, - Delay: 30 * time.Minute, - Interval: 60 * time.Minute, + Delay: setting.CloudBrainPayDelay, + Interval: setting.CloudBrainPayInterval, UnitAmount: spec.UnitPrice, RewardType: models.RewardTypePoint, StartTime: time.Unix(int64(task.StartTime), 0), + Tittle: RUN_CLOUDBRAIN_TASK_TITTLE, }) } @@ -61,7 +64,7 @@ func StopCloudBrainPointDeductTask(task models.Cloudbrain) { } func getCloudBrainPointTaskSourceId(task models.Cloudbrain) string { - return models.SourceTypeRunCloudbrainTask.Name() + "_" + task.JobType + "_" + fmt.Sprint(task.Type) + "_" + fmt.Sprint(task.ID) + return fmt.Sprint(task.ID) } func getResourceSpec(jobType string, resourceSpecId int) *models.ResourceSpec { @@ -100,11 +103,11 @@ func StartCloudbrainPointDeductTask() { }() log.Debug("try to run CloudbrainPointDeductTask") end := time.Now() - start := end.Add(5 * time.Minute) + start := end.Add(-5 * time.Minute) if firstTimeFlag { //When it is executed for the first time, it needs to process the tasks of the last 1 hours. //This is done to prevent the application from hanging for a long time - start = end.Add(1 * time.Hour) + start = end.Add(-1 * time.Hour) firstTimeFlag = false } diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go index f094e3a43..88e72a1a1 100644 --- a/services/reward/limiter/limiter.go +++ b/services/reward/limiter/limiter.go @@ -12,14 +12,6 @@ import ( "time" ) -type limiterRejectPolicy string - -const ( - JustReject limiterRejectPolicy = "JUST_REJECT" - PermittedOnce limiterRejectPolicy = "PERMITTED_ONCE" - FillUp limiterRejectPolicy = "FillUp" -) - type limiterRunner struct { limiters []models.LimitConfig index int @@ -27,7 +19,7 @@ type limiterRunner struct { amount int64 limitCode string limitType models.LimitType - rejectPolicy limiterRejectPolicy + rejectPolicy models.LimiterRejectPolicy resultMap map[int]limitResult minRealAmount int64 } @@ -46,7 +38,7 @@ func newLimitResult(isLoss bool, planAmount int64, realAmount int64) limitResult } } -func newLimiterRunner(limitCode string, limitType models.LimitType, userId, amount int64, policy limiterRejectPolicy) *limiterRunner { +func newLimiterRunner(limitCode string, limitType models.LimitType, userId, amount int64, policy models.LimiterRejectPolicy) *limiterRunner { return &limiterRunner{ userId: userId, amount: amount, @@ -149,7 +141,7 @@ func (l *limiterRunner) limit(r models.LimitConfig) error { usedNum, err = redis_client.IncrBy(redisKey, n) } if p != nil { - redis_client.Expire(redisKey, int64(p.LeftTime.Seconds())) + redis_client.Expire(redisKey, p.LeftTime) } } if usedNum > r.LimitNum { @@ -158,16 +150,16 @@ func (l *limiterRunner) limit(r models.LimitConfig) error { return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) } switch l.rejectPolicy { - case FillUp: + case models.FillUp: exceed := usedNum - r.LimitNum realAmount := l.amount - exceed redis_client.IncrBy(redisKey, -1*exceed) l.resultMap[l.index] = newLimitResult(true, l.amount, realAmount) return nil - case JustReject: + case models.JustReject: redis_client.IncrBy(redisKey, -1*l.amount) return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) - case PermittedOnce: + case models.PermittedOnce: l.resultMap[l.index] = newLimitResult(false, l.amount, l.amount) return nil } @@ -200,8 +192,11 @@ func (l *limiterRunner) countInPeriod(r models.LimitConfig, p *models.PeriodResu } } -func CheckLimitWithFillUp(limitCode string, limitType models.LimitType, userId, amount int64) (int64, error) { - r := newLimiterRunner(limitCode, limitType, userId, amount, FillUp) +func CheckLimit(limitCode string, limitType models.LimitType, userId, amount int64, rejectPolicy models.LimiterRejectPolicy) (int64, error) { + if rejectPolicy == "" { + rejectPolicy = models.JustReject + } + r := newLimiterRunner(limitCode, limitType, userId, amount, rejectPolicy) err := r.Run() if err != nil { return 0, err @@ -209,18 +204,6 @@ func CheckLimitWithFillUp(limitCode string, limitType models.LimitType, userId, return r.minRealAmount, nil } -func CheckLimitWithPermittedOnce(limitCode string, limitType models.LimitType, userId, amount int64) error { - r := newLimiterRunner(limitCode, limitType, userId, amount, PermittedOnce) - err := r.Run() - return err -} - -func CheckLimit(limitCode string, limitType models.LimitType, userId, amount int64) error { - r := newLimiterRunner(limitCode, limitType, userId, amount, JustReject) - err := r.Run() - return err -} - func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitConfig, error) { limiters, err := GetLimitersByLimitType(limitType) if err != nil { diff --git a/services/reward/operator.go b/services/reward/operator.go index 50ec01ff3..865ac10d0 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -5,7 +5,6 @@ import ( "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/modules/util" "code.gitea.io/gitea/services/reward/point" "errors" "fmt" @@ -69,7 +68,7 @@ func Operate(ctx *models.RewardOperateContext) error { } //new reward operate record - recordId, err := initAwardOperateRecord(ctx) + recordId, err := initRewardOperateRecord(ctx) if err != nil { return err } @@ -110,9 +109,12 @@ func isHandled(sourceType string, requestId string, operateType string) (bool, e } -func initAwardOperateRecord(ctx *models.RewardOperateContext) (string, error) { +func initRewardOperateRecord(ctx *models.RewardOperateContext) (string, error) { + sn, err := generateOperateSerialNo(ctx.OperateType, ctx.Reward.Type) + if err != nil { + return "", err + } record := &models.RewardOperateRecord{ - RecordId: util.UUID(), UserId: ctx.TargetUserId, Amount: ctx.Reward.Amount, RewardType: ctx.Reward.Type.Name(), @@ -122,17 +124,22 @@ func initAwardOperateRecord(ctx *models.RewardOperateContext) (string, error) { OperateType: ctx.OperateType.Name(), Status: models.OperateStatusOperating, Remark: ctx.Remark, + Tittle: ctx.Tittle, + SerialNo: sn, } - _, err := models.InsertAwardOperateRecord(record) + _, err = models.InsertRewardOperateRecord(record) if err != nil { return "", err } - return record.RecordId, nil + return record.SerialNo, nil } func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (string, error) { + sn, err := generateOperateSerialNo(ctx.OperateType, ctx.RewardType) + if err != nil { + return "", err + } record := &models.RewardOperateRecord{ - RecordId: util.UUID(), UserId: ctx.TargetUserId, Amount: 0, RewardType: ctx.RewardType.Name(), @@ -142,12 +149,14 @@ func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (strin OperateType: ctx.OperateType.Name(), Status: models.OperateStatusOperating, Remark: ctx.Remark, + Tittle: ctx.Tittle, + SerialNo: sn, } - _, err := models.InsertAwardOperateRecord(record) + _, err = models.InsertRewardOperateRecord(record) if err != nil { return "", err } - return record.RecordId, nil + return record.SerialNo, nil } func updateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus string) error { @@ -230,5 +239,30 @@ func StopPeriodicTask(sourceType models.SourceType, sourceId string, operateType } now := time.Now() RunRewardTask(*task, now) - return models.StopPeriodicTask(task.ID, task.OperateRecordId, now) + return models.StopPeriodicTask(task.ID, task.OperateSerialNo, now) +} + +func generateOperateSerialNo(operateType models.RewardOperateType, rewardType models.RewardType) (string, error) { + s, err := GetSerialNoByRedis() + if err != nil { + return "", err + } + + switch operateType { + case models.OperateTypeIncrease: + s += "1" + case models.OperateTypeDecrease: + s += "2" + default: + s += "9" + } + + switch rewardType { + case models.RewardTypePoint: + s += "1" + default: + s += "9" + } + + return s, nil } diff --git a/services/reward/period_task.go b/services/reward/period_task.go index d00e8d0c4..846989652 100644 --- a/services/reward/period_task.go +++ b/services/reward/period_task.go @@ -15,7 +15,7 @@ func NewRewardPeriodicTask(operateRecordId string, opts *models.StartPeriodicTas task.DelaySeconds = int64(opts.Delay.Seconds()) task.IntervalSeconds = int64(opts.Interval.Seconds()) task.Amount = opts.UnitAmount - task.OperateRecordId = operateRecordId + task.OperateSerialNo = operateRecordId task.Status = models.PeriodicTaskStatusRunning task.NextExecuteTime = timeutil.TimeStamp(opts.StartTime.Add(opts.Delay).Unix()) @@ -54,9 +54,9 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) { return } defer lock.UnLock() - record, err := models.GetPointOperateRecordByRecordId(t.OperateRecordId) + record, err := models.GetPointOperateRecordBySerialNo(t.OperateSerialNo) if err != nil { - log.Error("RunRewardTask. GetPointOperateRecordByRecordId error. %v", err) + log.Error("RunRewardTask. GetPointOperateRecordBySerialNo error. %v", err) return } if record.Status != models.OperateStatusOperating { @@ -75,7 +75,7 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) { } err = operator.Operate(&models.RewardOperateContext{ SourceType: models.SourceTypeRunCloudbrainTask, - SourceId: t.OperateRecordId, + SourceId: t.OperateSerialNo, Reward: models.Reward{ Amount: n * t.Amount, Type: models.GetRewardTypeInstance(record.RewardType), diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 4b84cdd0c..51a3657ad 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -18,7 +18,7 @@ type PointOperator struct { } func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) bool { - realAmount, err := limiter.CheckLimitWithFillUp(ctx.SourceType.Name(), models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount) + realAmount, err := limiter.CheckLimit(ctx.SourceType.Name(), models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount, ctx.RejectPolicy) if err != nil { return true } diff --git a/services/reward/record.go b/services/reward/record.go new file mode 100644 index 000000000..ac28b3565 --- /dev/null +++ b/services/reward/record.go @@ -0,0 +1,20 @@ +package reward + +import "code.gitea.io/gitea/models" + +type RecordResponse struct { + Records []models.RewardOperateRecordShow + Total int64 +} + +func GetRewardRecordList(opts models.RewardRecordListOpts) (*RecordResponse, error) { + l, n, err := models.GetRewardRecordList(opts) + if err != nil { + return nil, err + } + r := make([]models.RewardOperateRecordShow, 0) + for _, v := range l { + r = append(r, v.ToShow()) + } + return &RecordResponse{Records: r, Total: n}, nil +} diff --git a/services/reward/serial.go b/services/reward/serial.go new file mode 100644 index 000000000..e9509c403 --- /dev/null +++ b/services/reward/serial.go @@ -0,0 +1,21 @@ +package reward + +import ( + "code.gitea.io/gitea/modules/redis/redis_client" + "code.gitea.io/gitea/modules/redis/redis_key" + "fmt" + "math/rand" + "time" +) + +func GetSerialNoByRedis() (string, error) { + now := time.Now() + n, err := redis_client.IncrBy(redis_key.RewardSerialCounter(now), 1) + if err != nil { + return "", err + } + if n == 1 { + redis_client.Expire(redis_key.RewardSerialCounter(now), 5*time.Minute) + } + return now.Format("200601021504") + fmt.Sprint(rand.Intn(10)) + fmt.Sprintf("%02d", n), nil +} diff --git a/services/task/task.go b/services/task/task.go index 4c85ce52e..c2c4861a3 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -9,17 +9,33 @@ import ( "fmt" ) -func Accomplish(userId int64, taskType string) { - go accomplish(userId, taskType) +func Accomplish(action models.Action) { + switch action.OpType { + case models.ActionCreateRepo, + models.ActionCreateImage: + if action.Repo.IsPrivate { + return + } + case models.ActionCreateDebugGPUTask, + models.ActionCreateDebugNPUTask, + models.ActionCreateTrainTask, + models.ActionCreateInferenceTask, + models.ActionCreateBenchMarkTask, + models.ActionCreateGPUTrainTask: + action.OpType = models.ActionCreateCloudbrainTask + } + go accomplish(action) } -func accomplish(userId int64, taskType string) error { +func accomplish(action models.Action) error { defer func() { if err := recover(); err != nil { combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) log.Error("PANIC:%v", combinedErr) } }() + userId := action.ActUserID + taskType := fmt.Sprint(action.OpType) //get task config config, err := GetTaskConfig(taskType) @@ -33,7 +49,7 @@ func accomplish(userId int64, taskType string) error { } //is limited? - if isLimited(userId, config) { + if isLimited(userId, config, limiter.JustReject) { log.Info("task accomplish maximum times are reached,userId=%d taskType=%s", userId, taskType) return nil } @@ -45,6 +61,7 @@ func accomplish(userId int64, taskType string) error { ConfigId: config.ID, TaskCode: config.TaskCode, UserId: userId, + ActionId: action.ID, }) if err != nil { return err @@ -54,6 +71,7 @@ func accomplish(userId int64, taskType string) error { reward.Operate(&models.RewardOperateContext{ SourceType: models.SourceTypeAccomplishTask, SourceId: logId, + Tittle: config.Tittle, Reward: models.Reward{ Amount: config.AwardAmount, Type: models.GetRewardTypeInstance(config.AwardType), @@ -61,13 +79,14 @@ func accomplish(userId int64, taskType string) error { TargetUserId: userId, RequestId: logId, OperateType: models.OperateTypeIncrease, + RejectPolicy: limiter.FillUp, }) return nil } -func isLimited(userId int64, config *models.TaskConfig) bool { - if err := limiter.CheckLimit(config.TaskCode, models.LimitTypeTask, userId, 1); err != nil { +func isLimited(userId int64, config *models.TaskConfig, rejectPolicy limiter.LimiterRejectPolicy) bool { + if _, err := limiter.CheckLimit(config.TaskCode, models.LimitTypeTask, userId, 1, rejectPolicy); err != nil { return true } return false From 06161c0d174d64232895da429b1e001ead3b6289 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 24 Jun 2022 17:56:59 +0800 Subject: [PATCH 015/283] #2225 update --- models/limit_config.go | 35 ++++++++++++++++++++++ models/reward_operate_record.go | 4 +-- modules/auth/wechat/client.go | 2 ++ modules/redis/redis_key/limit_redis_key.go | 4 +-- routers/authentication/wechat.go | 1 + routers/reward/point/limit.go | 9 ++++++ routers/reward/point/point.go | 2 +- routers/routes/routes.go | 1 + services/reward/admin_operate.go | 3 +- services/reward/limiter/config.go | 21 ++++++++++++- services/reward/limiter/limiter.go | 2 +- services/reward/operator.go | 7 +++-- services/reward/point/point_operate.go | 6 ++-- services/task/task.go | 6 ++-- services/task/task_config.go | 2 +- 15 files changed, 86 insertions(+), 19 deletions(-) diff --git a/models/limit_config.go b/models/limit_config.go index ce8d2cfc2..17f0c23a2 100644 --- a/models/limit_config.go +++ b/models/limit_config.go @@ -59,27 +59,33 @@ type LimitConfig struct { LimitType string `xorm:"NOT NULL"` CreatorId int64 `xorm:"NOT NULL"` CreatorName string + DeleterId int64 + DeleterName string CreatedUnix timeutil.TimeStamp `xorm:"created"` DeletedAt timeutil.TimeStamp `xorm:"deleted"` } type LimitConfigVO struct { + ID int64 Tittle string RefreshRate string Scope string LimitNum int64 LimitCode string + LimitType string Creator string CreatedUnix timeutil.TimeStamp } func (l *LimitConfig) ToLimitConfigVO() *LimitConfigVO { return &LimitConfigVO{ + ID: l.ID, Tittle: l.Tittle, RefreshRate: l.RefreshRate, Scope: l.Scope, LimitNum: l.LimitNum, LimitCode: l.LimitCode, + LimitType: l.LimitType, Creator: l.CreatorName, CreatedUnix: l.CreatedUnix, } @@ -128,3 +134,32 @@ func AddLimitConfig(l *LimitConfig) error { sess.Commit() return nil } + +func DeleteLimitConfig(config LimitConfig, deleterId int64, deleterName string) error { + sess := x.NewSession() + defer sess.Close() + + _, err := x.ID(config.ID).Update(&LimitConfig{DeleterName: deleterName, DeleterId: deleterId}) + if err != nil { + sess.Rollback() + return err + } + _, err = x.ID(config.ID).Delete(&LimitConfig{}) + if err != nil { + sess.Rollback() + return err + } + sess.Commit() + return nil +} + +func GetLimitConfigById(id int64) (*LimitConfig, error) { + r := &LimitConfig{} + isOk, err := x.ID(id).Get(r) + if err != nil { + return nil, err + } else if !isOk { + return nil, nil + } + return r, nil +} diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index d58accfa5..889d291fa 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -109,7 +109,7 @@ const Semicolon = ";" type RewardOperateOrderBy string const ( - RewardOrderByID RewardOperateOrderBy = "id" + RewardOrderByIDDesc RewardOperateOrderBy = "id desc" ) type RewardOperateRecord struct { @@ -253,7 +253,7 @@ func GetRewardRecordList(opts RewardRecordListOpts) ([]RewardOperateRecord, int6 } if len(opts.OrderBy) == 0 { - opts.OrderBy = RewardOrderByID + opts.OrderBy = RewardOrderByIDDesc } r := make([]RewardOperateRecord, 0) diff --git a/modules/auth/wechat/client.go b/modules/auth/wechat/client.go index 6734977a1..5a81aa808 100644 --- a/modules/auth/wechat/client.go +++ b/modules/auth/wechat/client.go @@ -66,6 +66,7 @@ func getWechatRestyClient() *resty.Client { func callAccessToken() *AccessTokenResponse { client := getWechatRestyClient() + log.Info("start to get wechat access token") var result AccessTokenResponse _, err := client.R(). SetQueryParam("grant_type", GRANT_TYPE). @@ -77,6 +78,7 @@ func callAccessToken() *AccessTokenResponse { log.Error("get wechat access token failed,e=%v", err) return nil } + log.Info("get wechat access token result=%v", result) return &result } diff --git a/modules/redis/redis_key/limit_redis_key.go b/modules/redis/redis_key/limit_redis_key.go index a58a70fdb..02c4b1b9a 100644 --- a/modules/redis/redis_key/limit_redis_key.go +++ b/modules/redis/redis_key/limit_redis_key.go @@ -21,6 +21,6 @@ func LimitCount(userId int64, limitCode string, limitType string, scope string, } -func LimitConfig(limitType models.LimitType) string { - return KeyJoin(LIMIT_REDIS_PREFIX, limitType.Name(), "config") +func LimitConfig(limitType string) string { + return KeyJoin(LIMIT_REDIS_PREFIX, limitType, "config") } diff --git a/routers/authentication/wechat.go b/routers/authentication/wechat.go index 72871afb3..152348125 100644 --- a/routers/authentication/wechat.go +++ b/routers/authentication/wechat.go @@ -29,6 +29,7 @@ func GetQRCode4Bind(ctx *context.Context) { r, err := createQRCode4Bind(userId) if err != nil { + log.Error("GetQRCode4Bind failed,error=%v", err) ctx.JSON(200, map[string]interface{}{ "code": "9999", "msg": "Get QR code failed", diff --git a/routers/reward/point/limit.go b/routers/reward/point/limit.go index a831169f8..6c5ec5827 100644 --- a/routers/reward/point/limit.go +++ b/routers/reward/point/limit.go @@ -25,3 +25,12 @@ func AddPointLimitConfig(ctx *context.Context, config models.LimitConfigVO) { } ctx.JSON(http.StatusOK, response.Success()) } +func DeletePointLimitConfig(ctx *context.Context) { + id := ctx.QueryInt64("id") + err := limiter.DeleteLimitConfig(id, ctx.User) + if err != nil { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + return + } + ctx.JSON(http.StatusOK, response.Success()) +} diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index 3140b4c38..e1a751495 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -38,7 +38,7 @@ func GetPointRecordList(ctx *context.Context) { var orderBy models.RewardOperateOrderBy switch ctx.Query("sort") { default: - orderBy = models.RewardOrderByID + orderBy = models.RewardOrderByIDDesc } t := models.GetRewardOperateTypeInstance(operateType) if t == "" { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 0658765ca..652560119 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -595,6 +595,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/reward/point", func() { m.Get("/limiter/list", point.GetPointLimitConfigList) m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) + m.Post("/limiter/delete", point.DeletePointLimitConfig) m.Post("/operate", binding.Bind(models.AdminRewardOperateReq{}), point.OperatePointAccountBalance) }) diff --git a/services/reward/admin_operate.go b/services/reward/admin_operate.go index 1eec0f414..1fdd942d2 100644 --- a/services/reward/admin_operate.go +++ b/services/reward/admin_operate.go @@ -4,7 +4,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/services/reward/limiter" ) func AdminBalanceOperate(req models.AdminRewardOperateReq, doer *models.User) error { @@ -37,7 +36,7 @@ func AdminBalanceOperate(req models.AdminRewardOperateReq, doer *models.User) er RequestId: logId, OperateType: req.OperateType, Remark: req.Remark, - RejectPolicy: limiter.JustReject, + RejectPolicy: models.JustReject, }) if err != nil { diff --git a/services/reward/limiter/config.go b/services/reward/limiter/config.go index 12204b2c5..8de00d178 100644 --- a/services/reward/limiter/config.go +++ b/services/reward/limiter/config.go @@ -18,6 +18,9 @@ func GetLimitConfigList(limitType models.LimitType) ([]*models.LimitConfigVO, er } return result, nil } +func GetLimitConfigById(id int64) (*models.LimitConfig, error) { + return models.GetLimitConfigById(id) +} func AddLimitConfig(config *models.LimitConfigVO, doer *models.User, limitType models.LimitType) error { r := &models.LimitConfig{ @@ -36,6 +39,22 @@ func AddLimitConfig(config *models.LimitConfigVO, doer *models.User, limitType m log.Error("add limit config error,config:%v err:%v", config, err) return err } - redis_client.Del(redis_key.LimitConfig(limitType)) + redis_client.Del(redis_key.LimitConfig(limitType.Name())) + return nil +} + +func DeleteLimitConfig(id int64, doer *models.User) error { + config, err := GetLimitConfigById(id) + if err != nil { + log.Error("GetLimitConfigById err,e=%v", err) + return err + } + err = models.DeleteLimitConfig(*config, doer.ID, doer.Name) + + if err != nil { + log.Error("add limit config error,config:%v err:%v", config, err) + return err + } + redis_client.Del(redis_key.LimitConfig(config.LimitType)) return nil } diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go index 88e72a1a1..a73779ac1 100644 --- a/services/reward/limiter/limiter.go +++ b/services/reward/limiter/limiter.go @@ -219,7 +219,7 @@ func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitCo } func GetLimitersByLimitType(limitType models.LimitType) ([]models.LimitConfig, error) { - redisKey := redis_key.LimitConfig(limitType) + redisKey := redis_key.LimitConfig(limitType.Name()) val, _ := redis_client.Get(redisKey) if val != "" { if val == redis_key.EMPTY_REDIS_VAL { diff --git a/services/reward/operator.go b/services/reward/operator.go index 865ac10d0..79f82acbd 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -16,7 +16,7 @@ var RewardOperatorMap = map[string]RewardOperator{ } type RewardOperator interface { - IsLimited(ctx *models.RewardOperateContext) bool + IsLimited(ctx *models.RewardOperateContext) error Operate(ctx *models.RewardOperateContext) error } @@ -62,8 +62,9 @@ func Operate(ctx *models.RewardOperateContext) error { if ctx.OperateType == models.OperateTypeIncrease { //is limited? - if isLimited := operator.IsLimited(ctx); isLimited { - return nil + if err := operator.IsLimited(ctx); err != nil { + log.Info("operator IsLimited, err=%v", err) + return err } } diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 51a3657ad..1a4ff762b 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -17,16 +17,16 @@ const LossMsg = "达到奖励上限,应得%d积分,实得%d积分" type PointOperator struct { } -func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) bool { +func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) error { realAmount, err := limiter.CheckLimit(ctx.SourceType.Name(), models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount, ctx.RejectPolicy) if err != nil { - return true + return err } if realAmount < ctx.Reward.Amount { ctx.Remark = models.AppendRemark(ctx.Remark, fmt.Sprintf(LossMsg, ctx.Reward.Amount, realAmount)) ctx.Reward.Amount = realAmount } - return false + return nil } func (operator *PointOperator) Operate(ctx *models.RewardOperateContext) error { diff --git a/services/task/task.go b/services/task/task.go index c2c4861a3..0dfc38b6c 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -49,7 +49,7 @@ func accomplish(action models.Action) error { } //is limited? - if isLimited(userId, config, limiter.JustReject) { + if isLimited(userId, config, models.JustReject) { log.Info("task accomplish maximum times are reached,userId=%d taskType=%s", userId, taskType) return nil } @@ -79,13 +79,13 @@ func accomplish(action models.Action) error { TargetUserId: userId, RequestId: logId, OperateType: models.OperateTypeIncrease, - RejectPolicy: limiter.FillUp, + RejectPolicy: models.FillUp, }) return nil } -func isLimited(userId int64, config *models.TaskConfig, rejectPolicy limiter.LimiterRejectPolicy) bool { +func isLimited(userId int64, config *models.TaskConfig, rejectPolicy models.LimiterRejectPolicy) bool { if _, err := limiter.CheckLimit(config.TaskCode, models.LimitTypeTask, userId, 1, rejectPolicy); err != nil { return true } diff --git a/services/task/task_config.go b/services/task/task_config.go index 4e02b4972..0184ca15b 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -95,7 +95,7 @@ func AddTaskConfig(config models.TaskConfigWithLimit, doer *models.User) error { log.Error("add task config error,config:%v err:%v", config, err) return err } - redis_client.Del(redis_key.LimitConfig(models.LimitTypeTask)) + redis_client.Del(redis_key.LimitConfig(models.LimitTypeTask.Name())) redis_client.Del(redis_key.TaskConfigList()) return nil } From ee415d75aba7609b18272688ae9ac10b57804a28 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Mon, 27 Jun 2022 12:05:51 +0800 Subject: [PATCH 016/283] #2225 fix bug --- services/reward/notify.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/reward/notify.go b/services/reward/notify.go index aa18fbe39..f5b270d94 100644 --- a/services/reward/notify.go +++ b/services/reward/notify.go @@ -18,11 +18,11 @@ func NotifyRewardOperation(userId, amount int64, rewardType models.RewardType, o OperateType: operateType, } b, _ := json.Marshal(data) - redis_client.ZAdd(redis_key.RewardOperateNotification(), string(b), float64(time.Now().UnixMilli())) + redis_client.ZAdd(redis_key.RewardOperateNotification(), string(b), float64(time.Now().Unix())) } func GetRewardOperation(since, until timeutil.TimeStamp) []models.UserRewardOperation { - list, err := redis_client.ZRangeByScore(redis_key.RewardOperateNotification(), float64(since*1000), float64(until*1000)) + list, err := redis_client.ZRangeByScore(redis_key.RewardOperateNotification(), float64(since), float64(until)) if err != nil { return nil } @@ -38,7 +38,7 @@ func GetRewardOperation(since, until timeutil.TimeStamp) []models.UserRewardOper Msg: GetRewardOperateMsg(t), }) } - redis_client.ZRemRangeByScore(redis_key.RewardOperateNotification(), float64(since*1000), float64(until*1000)) + redis_client.ZRemRangeByScore(redis_key.RewardOperateNotification(), float64(since), float64(until)) return r } From 93317c7a81b102e4b8a7002f97827f40a0bb95a4 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Mon, 27 Jun 2022 17:44:14 +0800 Subject: [PATCH 017/283] #2225 add point page --- models/reward_operate_record.go | 27 +++++++++------- routers/reward/point/point.go | 8 ++++- routers/routes/routes.go | 1 + services/reward/admin_operate.go | 11 ++++--- services/reward/period_task.go | 45 +++++++++++++++++--------- services/reward/point/point_operate.go | 5 +++ services/reward/record.go | 8 +++-- 7 files changed, 70 insertions(+), 35 deletions(-) diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 889d291fa..394fba1cf 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -141,18 +141,22 @@ func (r RewardOperateRecord) ToShow() RewardOperateRecordShow { return RewardOperateRecordShow{ SerialNo: r.SerialNo, Date: r.CreatedUnix, - Tittle: r.Tittle, OperateType: r.OperateType, Amount: r.Amount, + Remark: r.Remark, } } type RewardOperateRecordShow struct { SerialNo string Date timeutil.TimeStamp - Tittle string + Status string OperateType string Amount int64 + Action Action + Cloudbrain Cloudbrain + SourceType SourceType + Remark string } func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error) { @@ -207,15 +211,16 @@ func SumRewardAmountInTaskPeriod(rewardType string, sourceType string, userId in } type RewardOperateContext struct { - SourceType SourceType - SourceId string - Tittle string - Remark string - Reward Reward - TargetUserId int64 - RequestId string - OperateType RewardOperateType - RejectPolicy LimiterRejectPolicy + SourceType SourceType + SourceId string + Tittle string + Remark string + Reward Reward + TargetUserId int64 + RequestId string + OperateType RewardOperateType + RejectPolicy LimiterRejectPolicy + PermittedNegative bool } type Reward struct { diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index e1a751495..edf41cd72 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -2,6 +2,7 @@ package point import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/routers/response" "code.gitea.io/gitea/services/reward" @@ -9,6 +10,8 @@ import ( "net/http" ) +const tplPoint base.TplName = "/reward/point" + type AccountResponse struct { AccountCode string Balance int64 @@ -24,7 +27,6 @@ func GetPointAccount(ctx *context.Context) { return } res := &AccountResponse{ - AccountCode: a.AccountCode, Balance: a.Balance, TotalEarned: a.TotalEarned, TotalConsumed: a.TotalConsumed, @@ -75,3 +77,7 @@ func OperatePointAccountBalance(ctx *context.Context, req models.AdminRewardOper } ctx.JSON(http.StatusOK, response.Success()) } + +func GetPointPage(ctx *context.Context) { + ctx.HTML(200, tplPoint) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 946f9ddd2..ea2980364 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -595,6 +595,7 @@ func RegisterRoutes(m *macaron.Macaron) { }) m.Group("/reward/point", func() { + m.Get("", point.GetPointPage) m.Get("/limiter/list", point.GetPointLimitConfigList) m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) m.Post("/limiter/delete", point.DeletePointLimitConfig) diff --git a/services/reward/admin_operate.go b/services/reward/admin_operate.go index 1fdd942d2..b8490a3a8 100644 --- a/services/reward/admin_operate.go +++ b/services/reward/admin_operate.go @@ -32,11 +32,12 @@ func AdminBalanceOperate(req models.AdminRewardOperateReq, doer *models.User) er Amount: req.Amount, Type: req.RewardType, }, - TargetUserId: req.TargetUserId, - RequestId: logId, - OperateType: req.OperateType, - Remark: req.Remark, - RejectPolicy: models.JustReject, + TargetUserId: req.TargetUserId, + RequestId: logId, + OperateType: req.OperateType, + Remark: req.Remark, + RejectPolicy: models.JustReject, + PermittedNegative: true, }) if err != nil { diff --git a/services/reward/period_task.go b/services/reward/period_task.go index 846989652..3fa416dab 100644 --- a/services/reward/period_task.go +++ b/services/reward/period_task.go @@ -63,31 +63,41 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) { log.Info("RunRewardTask. operate record is finished,record=%+v", record) return } - n, nextTime := countExecuteTimes(t, now) + n, _ := countExecuteTimes(t, now) if n == 0 { return } + //get operator operator := GetOperator(models.GetRewardTypeInstance(record.RewardType)) if operator == nil { log.Error("RunRewardTask. operator of reward type is not exist") return } - err = operator.Operate(&models.RewardOperateContext{ - SourceType: models.SourceTypeRunCloudbrainTask, - SourceId: t.OperateSerialNo, - Reward: models.Reward{ - Amount: n * t.Amount, - Type: models.GetRewardTypeInstance(record.RewardType), - }, - TargetUserId: record.UserId, - OperateType: models.GetRewardOperateTypeInstance(record.OperateType), - }) - if err != nil { - log.Error("RunRewardTask.operator operate error.%v", err) - return + nextTime := t.NextExecuteTime + for i := 0; int64(i) <= n; i++ { + err = operator.Operate(&models.RewardOperateContext{ + SourceType: models.SourceTypeRunCloudbrainTask, + SourceId: t.OperateSerialNo, + Reward: models.Reward{ + Amount: t.Amount, + Type: models.GetRewardTypeInstance(record.RewardType), + }, + TargetUserId: record.UserId, + OperateType: models.GetRewardOperateTypeInstance(record.OperateType), + }) + if err != nil { + log.Error("RunRewardTask.operator operate error.%v", err) + if models.IsErrInsufficientPointsBalance(err) { + StopCloudbrainTask(record) + return + } + return + } + models.IncrRewardTaskSuccessCount(t, n, nextTime) + nextTime = timeutil.TimeStamp(int64(nextTime) + t.IntervalSeconds) } - models.IncrRewardTaskSuccessCount(t, n, nextTime) + } func countExecuteTimes(t models.RewardPeriodicTask, now time.Time) (int64, timeutil.TimeStamp) { @@ -101,3 +111,8 @@ func countExecuteTimes(t models.RewardPeriodicTask, now time.Time) (int64, timeu newNextTime := timeutil.TimeStamp(nextTime + n*interval) return n, newNextTime } + +func StopCloudbrainTask(r *models.RewardOperateRecord) { + //todo + +} diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 1a4ff762b..0115c288a 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -2,6 +2,7 @@ package point import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/modules/redis/redis_lock" @@ -46,6 +47,10 @@ func (operator *PointOperator) Operate(ctx *models.RewardOperateContext) error { if ctx.OperateType == models.OperateTypeIncrease { err = na.Increase(ctx.Reward.Amount, ctx.SourceId) } else if ctx.OperateType == models.OperateTypeDecrease { + if !ctx.PermittedNegative && na.Balance < ctx.Reward.Amount { + log.Info("account balance is not enough,ctx=%v", ctx) + return &models.ErrInsufficientPointsBalance{} + } err = na.Decrease(ctx.Reward.Amount, ctx.SourceId) } if err != nil { diff --git a/services/reward/record.go b/services/reward/record.go index ac28b3565..157e53b53 100644 --- a/services/reward/record.go +++ b/services/reward/record.go @@ -3,8 +3,10 @@ package reward import "code.gitea.io/gitea/models" type RecordResponse struct { - Records []models.RewardOperateRecordShow - Total int64 + Records []models.RewardOperateRecordShow + Total int64 + PageSize int + Page int } func GetRewardRecordList(opts models.RewardRecordListOpts) (*RecordResponse, error) { @@ -16,5 +18,5 @@ func GetRewardRecordList(opts models.RewardRecordListOpts) (*RecordResponse, err for _, v := range l { r = append(r, v.ToShow()) } - return &RecordResponse{Records: r, Total: n}, nil + return &RecordResponse{Records: r, Total: n, Page: opts.Page, PageSize: opts.PageSize}, nil } From 2280f0be44f365d698fdf8ee082f036736b03b53 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Mon, 27 Jun 2022 18:03:34 +0800 Subject: [PATCH 018/283] #2225 fix point page path --- routers/routes/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index ea2980364..47ee3c50a 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -595,7 +595,6 @@ func RegisterRoutes(m *macaron.Macaron) { }) m.Group("/reward/point", func() { - m.Get("", point.GetPointPage) m.Get("/limiter/list", point.GetPointLimitConfigList) m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) m.Post("/limiter/delete", point.DeletePointLimitConfig) @@ -1337,6 +1336,7 @@ func RegisterRoutes(m *macaron.Macaron) { }, reqSignIn) m.Group("/reward/point", func() { + m.Get("", point.GetPointPage) m.Get("/account", point.GetPointAccount) m.Get("/record/list", point.GetPointRecordList) }, reqSignIn) From 44586c567ba4842335b006559416bc1cb610a6a5 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 28 Jun 2022 18:14:23 +0800 Subject: [PATCH 019/283] #2225 update --- models/action.go | 15 ++ models/cloudbrain.go | 110 ++++++++++++- models/reward_admin_log.go | 15 ++ models/reward_operate_record.go | 147 +++++++++++++++--- models/reward_periodic_task.go | 2 +- models/task_accomplish_log.go | 1 - modules/cron/tasks_basic.go | 2 +- routers/repo/cloudbrain.go | 10 +- routers/repo/modelarts.go | 10 +- routers/reward/point/point.go | 1 - services/reward/cloubrain_deduct.go | 82 ++++------ services/reward/operator.go | 51 +++--- services/reward/period_task.go | 33 ++-- .../reward/point/account/point_account.go | 18 +++ services/reward/record.go | 18 ++- services/task/task.go | 7 +- 16 files changed, 386 insertions(+), 136 deletions(-) diff --git a/models/action.go b/models/action.go index 456d5c6bc..ff16dcd3f 100755 --- a/models/action.go +++ b/models/action.go @@ -412,3 +412,18 @@ func GetUnTransformedActions() ([]*Action, error) { Find(&actions) return actions, err } + +func GetActionByIds(ids []int64) ([]*Action, error) { + if len(ids) == 0 { + return nil, nil + } + actions := make([]*Action, 0) + err := x.In("id", ids).Find(&actions) + if err != nil { + return nil, err + } + if err := ActionList(actions).LoadAttributes(); err != nil { + return nil, fmt.Errorf("ActionList loadAttributes: %v", err) + } + return actions, nil +} diff --git a/models/cloudbrain.go b/models/cloudbrain.go index 33b85de20..06cd42258 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -168,6 +168,72 @@ type Cloudbrain struct { EndTime timeutil.TimeStamp } +type CloudbrainShow struct { + JobID string `xorm:"INDEX NOT NULL"` + JobType string `xorm:"INDEX NOT NULL DEFAULT 'DEBUG'"` + JobName string + DisplayJobName string + Status string + UserID int64 `xorm:"INDEX NOT NULL"` + RepoID int64 `xorm:"INDEX NOT NULL"` + SubTaskName string + ContainerID string + ContainerIp string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + Duration int64 `xorm:"DEFAULT 0"` //运行时长 单位秒 + TrainJobDuration string `xorm:"DEFAULT '00:00:00'"` + Image string //镜像名称 + GpuQueue string //GPU类型即GPU队列 + ResourceSpecId int //GPU规格id + DeletedAt time.Time `xorm:"deleted"` + CanDebug bool `xorm:"-"` + CanDel bool `xorm:"-"` + CanModify bool `xorm:"-"` + Type int + BenchmarkTypeID int + BenchmarkChildTypeID int + + VersionID int64 //版本id + VersionName string `xorm:"INDEX"` //当前版本 + Uuid string //数据集id + DatasetName string + VersionCount int //任务的当前版本数量,不包括删除的 + IsLatestVersion string //是否是最新版本,1是,0否 + CommitID string //提交的仓库代码id + PreVersionName string //父版本名称 + ComputeResource string //计算资源,例如npu + EngineID int64 //引擎id + + TrainUrl string //输出模型的obs路径 + BranchName string //分支名称 + Parameters string //传给modelarts的param参数 + BootFile string //启动文件 + DataUrl string //数据集的obs路径 + LogUrl string //日志输出的obs路径 + PreVersionId int64 //父版本的版本id + FlavorCode string //modelarts上的规格id + Description string `xorm:"varchar(256)"` //描述 + WorkServerNumber int //节点数 + FlavorName string //规格名称 + EngineName string //引擎名称 + TotalVersionCount int //任务的所有版本数量,包括删除的 + + LabelName string //标签名称 + ModelName string //模型名称 + ModelVersion string //模型版本 + CkptName string //权重文件名称 + ResultUrl string //推理结果的obs路径 + + User *User `xorm:"-"` + Repo *Repository `xorm:"-"` + BenchmarkType string `xorm:"-"` //算法评测,模型评测 + BenchmarkTypeName string `xorm:"-"` + BenchmarkTypeRankLink string `xorm:"-"` + StartTime timeutil.TimeStamp + EndTime timeutil.TimeStamp +} + func (task *Cloudbrain) ComputeAndSetDuration() { var d int64 if task.StartTime == 0 { @@ -1844,9 +1910,51 @@ func CloudbrainAllStatic(opts *CloudbrainsOptions) ([]*CloudbrainInfo, int64, er func GetStartedCloudbrainTaskByUpdatedUnix(startTime, endTime time.Time) ([]Cloudbrain, error) { r := make([]Cloudbrain, 0) - err := x.Where("updated_unix >= ? and updated_unix <= ? and start_time > 0", startTime.Unix(), endTime.Unix()).Find(&r) + err := x.Where("updated_unix >= ? and updated_unix <= ? and start_time > 0", startTime.Unix(), endTime.Unix()).Unscoped().Find(&r) if err != nil { return nil, err } return r, nil } + +func GetCloudbrainByIds(ids []int64) ([]Cloudbrain, error) { + if len(ids) == 0 { + return nil, nil + } + cloudbrains := make([]Cloudbrain, 0) + err := x.In("id", ids).Unscoped().Find(&cloudbrains) + if err != nil { + return nil, err + } + return cloudbrains, nil +} + +var ( + DebugResourceSpecs *ResourceSpecs + TrainResourceSpecs *ResourceSpecs +) + +func GetResourceSpec(jobType string, resourceSpecId int) *ResourceSpec { + if jobType == string(JobTypeTrain) { + if TrainResourceSpecs == nil { + json.Unmarshal([]byte(setting.TrainResourceSpecs), &TrainResourceSpecs) + } + for _, spec := range TrainResourceSpecs.ResourceSpec { + if resourceSpecId == spec.Id { + return spec + } + } + } else { + if DebugResourceSpecs == nil { + json.Unmarshal([]byte(setting.ResourceSpecs), &DebugResourceSpecs) + } + for _, spec := range DebugResourceSpecs.ResourceSpec { + if resourceSpecId == spec.Id { + return spec + } + } + + } + return nil + +} diff --git a/models/reward_admin_log.go b/models/reward_admin_log.go index 5e4258682..b1a55af13 100644 --- a/models/reward_admin_log.go +++ b/models/reward_admin_log.go @@ -2,6 +2,7 @@ package models import ( "code.gitea.io/gitea/modules/timeutil" + "strings" ) const ( @@ -44,3 +45,17 @@ func UpdateRewardAdminLogStatus(logId string, oldStatus, newStatus int) error { } return nil } + +func GetRewardAdminLogByLogIds(logIds []string) ([]RewardAdminLog, error) { + if len(logIds) == 0 { + return nil, nil + } + adminLogs := make([]RewardAdminLog, 0) + err := x.SQL("select rdl.id,rdl.log_id,rdl.amount,rdl.reward_type,rdl.remark,rdl.status,rdl.target_user_id,rdl.creator_id,u.name as creator_name "+ + "from reward_admin_log rdl left join public.user u on rdl.creator_id = u.id "+ + "where rdl.log_id in (?)", strings.Join(logIds, ",")).Find(&adminLogs) + if err != nil { + return nil, err + } + return adminLogs, nil +} diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 394fba1cf..04f43a8bd 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -2,6 +2,8 @@ package models import ( "code.gitea.io/gitea/modules/timeutil" + "fmt" + "strconv" "strings" "xorm.io/builder" ) @@ -112,21 +114,120 @@ const ( RewardOrderByIDDesc RewardOperateOrderBy = "id desc" ) +type RewardRecordList []*RewardOperateRecord +type RewardRecordShowList []*RewardOperateRecordShow + +func (l *RewardRecordList) ToShow() (RewardRecordShowList, error) { + actionMap, err := l.GetRewardRecordAction() + adminLogMap, err := l.GetRewardRecordAdminLog() + CloudbrainMap, err := l.GetRewardRecordCloudbrainTask() + if err != nil { + return nil, err + } + result := make([]*RewardOperateRecordShow, 0) + for _, v := range *l { + temp := v.ToShow() + switch v.SourceType { + case SourceTypeAccomplishTask.Name(): + temp.Action = actionMap[v.SourceId] + case SourceTypeAdminOperate.Name(): + temp.AdminLog = adminLogMap[v.SourceId] + case SourceTypeRunCloudbrainTask.Name(): + temp.Cloudbrain = CloudbrainMap[v.SourceId] + } + result = append(result, &temp) + } + + return result, nil +} + +func (l *RewardRecordList) GetRewardRecordAction() (map[string]Action, error) { + if len(*l) == 0 { + return nil, nil + } + actionIds := make([]int64, 0) + for _, r := range *l { + if r.SourceType != SourceTypeAccomplishTask.Name() { + continue + } + i, _ := strconv.ParseInt(r.SourceId, 10, 64) + actionIds = append(actionIds, i) + } + actions, err := GetActionByIds(actionIds) + if err != nil { + return nil, err + } + result := make(map[string]Action, 0) + for _, v := range actions { + result[fmt.Sprint(v.ID)] = *v + } + return result, nil + +} + +func (l *RewardRecordList) GetRewardRecordAdminLog() (map[string]RewardAdminLog, error) { + if len(*l) == 0 { + return nil, nil + } + logIds := make([]string, 0) + for _, r := range *l { + if r.SourceType != SourceTypeAdminOperate.Name() { + continue + } + logIds = append(logIds, r.SourceId) + } + logs, err := GetRewardAdminLogByLogIds(logIds) + if err != nil { + return nil, err + } + result := make(map[string]RewardAdminLog, 0) + for _, v := range logs { + result[fmt.Sprint(v.LogId)] = v + } + return result, nil + +} + +func (l *RewardRecordList) GetRewardRecordCloudbrainTask() (map[string]Cloudbrain, error) { + if len(*l) == 0 { + return nil, nil + } + cloudbrainIds := make([]int64, 0) + for _, r := range *l { + if r.SourceType != SourceTypeRunCloudbrainTask.Name() { + continue + } + i, _ := strconv.ParseInt(r.SourceId, 10, 64) + cloudbrainIds = append(cloudbrainIds, i) + } + cloudbrains, err := GetCloudbrainByIds(cloudbrainIds) + if err != nil { + return nil, err + } + result := make(map[string]Cloudbrain, 0) + for _, v := range cloudbrains { + result[fmt.Sprint(v.ID)] = v + } + return result, nil + +} + type RewardOperateRecord struct { - ID int64 `xorm:"pk autoincr"` - SerialNo string `xorm:"INDEX NOT NULL"` - UserId int64 `xorm:"INDEX NOT NULL"` - Amount int64 `xorm:"NOT NULL"` - Tittle string - RewardType string `xorm:"NOT NULL"` - SourceType string `xorm:"NOT NULL"` - SourceId string `xorm:"INDEX NOT NULL"` - RequestId string `xorm:"INDEX NOT NULL"` - OperateType string `xorm:"NOT NULL"` - Status string `xorm:"NOT NULL"` - Remark string - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + ID int64 `xorm:"pk autoincr"` + SerialNo string `xorm:"INDEX NOT NULL"` + UserId int64 `xorm:"INDEX NOT NULL"` + Amount int64 `xorm:"NOT NULL"` + Tittle string + RewardType string `xorm:"NOT NULL"` + SourceType string `xorm:"NOT NULL"` + SourceId string `xorm:"INDEX NOT NULL"` + RequestId string `xorm:"INDEX NOT NULL"` + OperateType string `xorm:"NOT NULL"` + Status string `xorm:"NOT NULL"` + Remark string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + FinishedUnix timeutil.TimeStamp `xorm:"INDEX"` } type AdminRewardOperateReq struct { @@ -144,6 +245,8 @@ func (r RewardOperateRecord) ToShow() RewardOperateRecordShow { OperateType: r.OperateType, Amount: r.Amount, Remark: r.Remark, + Status: r.Status, + SourceType: r.SourceType, } } @@ -153,10 +256,11 @@ type RewardOperateRecordShow struct { Status string OperateType string Amount int64 + Remark string + SourceType string Action Action Cloudbrain Cloudbrain - SourceType SourceType - Remark string + AdminLog RewardAdminLog } func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error) { @@ -189,11 +293,12 @@ func InsertRewardOperateRecord(tl *RewardOperateRecord) (int64, error) { return x.Insert(tl) } -func UpdateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus string) (int64, error) { +func UpdateRewardRecordToFinalStatus(sourceType, requestId, newStatus string) (int64, error) { r := &RewardOperateRecord{ - Status: newStatus, + Status: newStatus, + FinishedUnix: timeutil.TimeStampNow(), } - return x.Cols("status").Where("source_type=? and request_id=? and status=?", sourceType, requestId, oldStatus).Update(r) + return x.Cols("status", "finished_unix").Where("source_type=? and request_id=? and status=?", sourceType, requestId, OperateStatusOperating).Update(r) } func SumRewardAmountInTaskPeriod(rewardType string, sourceType string, userId int64, period *PeriodResult) (int64, error) { @@ -252,7 +357,7 @@ type RewardRecordListOpts struct { OrderBy RewardOperateOrderBy } -func GetRewardRecordList(opts RewardRecordListOpts) ([]RewardOperateRecord, int64, error) { +func GetRewardRecordList(opts RewardRecordListOpts) (RewardRecordList, int64, error) { if opts.Page <= 0 { opts.Page = 1 } @@ -261,7 +366,7 @@ func GetRewardRecordList(opts RewardRecordListOpts) ([]RewardOperateRecord, int6 opts.OrderBy = RewardOrderByIDDesc } - r := make([]RewardOperateRecord, 0) + r := make([]*RewardOperateRecord, 0) cond := builder.NewCond() if opts.UserId > 0 { cond = cond.And(builder.Eq{"user_id": opts.UserId}) diff --git a/models/reward_periodic_task.go b/models/reward_periodic_task.go index 5db5301b5..a859676d6 100644 --- a/models/reward_periodic_task.go +++ b/models/reward_periodic_task.go @@ -77,7 +77,7 @@ func IncrRewardTaskSuccessCount(t RewardPeriodicTask, count int64, nextTime time sess.Rollback() return err } - _, err = sess.Exec("update reward_operate_record set amount = amount + ? ,updated_unix = ? where serial_no = ?", count*t.Amount, timeutil.TimeStampNow(), t.OperateSerialNo) + _, err = sess.Exec("update reward_operate_record set amount = amount + ? ,updated_unix = ? where serial_no = ?", t.Amount, timeutil.TimeStampNow(), t.OperateSerialNo) if err != nil { sess.Rollback() return err diff --git a/models/task_accomplish_log.go b/models/task_accomplish_log.go index a1edb71ee..75494bfa2 100644 --- a/models/task_accomplish_log.go +++ b/models/task_accomplish_log.go @@ -7,7 +7,6 @@ import ( type TaskAccomplishLog struct { ID int64 `xorm:"pk autoincr"` - LogId string `xorm:"INDEX NOT NULL"` ConfigId int64 `xorm:"NOT NULL"` TaskCode string `xorm:"NOT NULL"` UserId int64 `xorm:"INDEX NOT NULL"` diff --git a/modules/cron/tasks_basic.go b/modules/cron/tasks_basic.go index 5892699eb..7d6c7df33 100755 --- a/modules/cron/tasks_basic.go +++ b/modules/cron/tasks_basic.go @@ -251,6 +251,6 @@ func initBasicTasks() { registerSyncCloudbrainStatus() registerHandleOrgStatistic() - registerRewardPeriodTask() + //registerRewardPeriodTask() registerCloudbrainPointDeductTask() } diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index f628a6f0a..29c8b97bb 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -2,7 +2,7 @@ package repo import ( "bufio" - "code.gitea.io/gitea/services/reward" + "code.gitea.io/gitea/services/reward/point/account" "encoding/json" "errors" "fmt" @@ -230,7 +230,7 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { command = commandTrain } - if !reward.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tpl, &form) @@ -318,7 +318,7 @@ func CloudBrainRestart(ctx *context.Context) { var status = string(models.JobWaiting) task := ctx.Cloudbrain for { - if !reward.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) resultCode = "-1" errorMsg = models.ErrInsufficientPointsBalance{}.Error() @@ -1870,7 +1870,7 @@ func BenchMarkAlgorithmCreate(ctx *context.Context, form auth.CreateCloudBrainFo repo := ctx.Repo.Repository - if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplCloudBrainBenchmarkNew, &form) @@ -2032,7 +2032,7 @@ func ModelBenchmarkCreate(ctx *context.Context, form auth.CreateCloudBrainForm) tpl := tplCloudBrainBenchmarkNew command := cloudbrain.Command - if !reward.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tpl, &form) diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index 1fbf6c622..bff9ec525 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -2,7 +2,7 @@ package repo import ( "archive/zip" - "code.gitea.io/gitea/services/reward" + "code.gitea.io/gitea/services/reward/point/account" "encoding/json" "errors" "fmt" @@ -207,7 +207,7 @@ func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm repo := ctx.Repo.Repository resourceSpecId := form.ResourceSpecId - if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeDebug), resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeDebug), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsNotebookNew, &form) @@ -426,7 +426,7 @@ func NotebookManage(ctx *context.Context) { errorMsg = "you have no right to restart the job" break } - if !reward.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) resultCode = "-1" errorMsg = models.ErrInsufficientPointsBalance{}.Error() @@ -1002,7 +1002,7 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) EngineName := form.EngineName resourceSpecId := form.ResourceSpecId - if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeTrain), resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeTrain), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsTrainJobNew, &form) @@ -1851,7 +1851,7 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference ckptUrl := form.TrainUrl + form.CkptName - if !reward.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeInference), resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeInference), resourceSpecId) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) inferenceJobErrorNewDataPrepare(ctx, form) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsInferenceJobNew, &form) diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index edf41cd72..fa5e31afa 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -13,7 +13,6 @@ import ( const tplPoint base.TplName = "/reward/point" type AccountResponse struct { - AccountCode string Balance int64 TotalEarned int64 TotalConsumed int64 diff --git a/services/reward/cloubrain_deduct.go b/services/reward/cloubrain_deduct.go index ce23e2dc7..1e547a8a1 100644 --- a/services/reward/cloubrain_deduct.go +++ b/services/reward/cloubrain_deduct.go @@ -4,8 +4,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/services/reward/point/account" - "encoding/json" "fmt" "time" ) @@ -17,34 +15,17 @@ var ( const RUN_CLOUDBRAIN_TASK_TITTLE = "运行云脑任务" -//IsPointBalanceEnough check whether the user's point balance is bigger than task unit price -func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int) bool { +func StartAndGetCloudBrainPointDeductTask(task models.Cloudbrain) (*models.RewardPeriodicTask, error) { if !setting.CloudBrainPaySwitch { - return true - } - spec := getResourceSpec(jobType, resourceSpecId) - if spec == nil { - return true - } - a, error := account.GetAccount(targetUserId) - if error != nil { - return false - } - return a.Balance >= spec.UnitPrice - -} - -func StartCloudBrainPointDeductTask(task models.Cloudbrain) { - if !setting.CloudBrainPaySwitch { - return + return nil, nil } - spec := getResourceSpec(task.JobType, task.ResourceSpecId) + spec := models.GetResourceSpec(task.JobType, task.ResourceSpecId) if spec == nil || spec.UnitPrice == 0 { - return + return nil, nil } - StartPeriodicTask(&models.StartPeriodicTaskOpts{ + return StartAndGetPeriodicTask(&models.StartPeriodicTaskOpts{ SourceType: models.SourceTypeRunCloudbrainTask, SourceId: getCloudBrainPointTaskSourceId(task), TargetUserId: task.UserID, @@ -67,31 +48,6 @@ func getCloudBrainPointTaskSourceId(task models.Cloudbrain) string { return fmt.Sprint(task.ID) } -func getResourceSpec(jobType string, resourceSpecId int) *models.ResourceSpec { - if jobType == string(models.JobTypeTrain) { - if TrainResourceSpecs == nil { - json.Unmarshal([]byte(setting.TrainResourceSpecs), &TrainResourceSpecs) - } - for _, spec := range TrainResourceSpecs.ResourceSpec { - if resourceSpecId == spec.Id { - return spec - } - } - } else { - if ResourceSpecs == nil { - json.Unmarshal([]byte(setting.ResourceSpecs), &ResourceSpecs) - } - for _, spec := range ResourceSpecs.ResourceSpec { - if resourceSpecId == spec.Id { - return spec - } - } - - } - return nil - -} - var firstTimeFlag = true func StartCloudbrainPointDeductTask() { @@ -107,10 +63,9 @@ func StartCloudbrainPointDeductTask() { if firstTimeFlag { //When it is executed for the first time, it needs to process the tasks of the last 1 hours. //This is done to prevent the application from hanging for a long time - start = end.Add(-1 * time.Hour) + start = end.Add(-3 * time.Hour) firstTimeFlag = false } - taskList, err := models.GetStartedCloudbrainTaskByUpdatedUnix(start, end) if err != nil { log.Error("GetStartedCloudbrainTaskByUpdatedUnix error. %v", err) @@ -121,11 +76,30 @@ func StartCloudbrainPointDeductTask() { return } for _, t := range taskList { - if int64(t.StartTime) <= end.Unix() && int64(t.StartTime) >= start.Unix() { - StartCloudBrainPointDeductTask(t) + //初始化 period_task 和 operate_record + if int64(t.StartTime) > end.Unix() || int64(t.StartTime) < start.Unix() { + continue + } + + task, err := StartAndGetCloudBrainPointDeductTask(t) + if err != nil { + log.Error("run cloubrain point deduct task error,err=%v", err) + continue } + if task == nil { + continue + } + if task.Status == models.PeriodicTaskStatusFinished { + log.Info("Periodic task is finished") + continue + } + if int64(t.EndTime) <= end.Unix() && int64(t.EndTime) >= start.Unix() { - StopCloudBrainPointDeductTask(t) + endTime := time.Unix(int64(t.EndTime), 0) + RunRewardTask(*task, endTime) + models.StopPeriodicTask(task.ID, task.OperateSerialNo, endTime) + } else { + RunRewardTask(*task, end) } } } diff --git a/services/reward/operator.go b/services/reward/operator.go index 79f82acbd..fc51aa1c5 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -78,11 +78,11 @@ func Operate(ctx *models.RewardOperateContext) error { //operate if err := operator.Operate(ctx); err != nil { - updateAwardOperateRecordStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) + UpdateRewardRecordToFinalStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusFailed) return err } - updateAwardOperateRecordStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusOperating, models.OperateStatusSucceeded) + UpdateRewardRecordToFinalStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusSucceeded) NotifyRewardOperation(ctx.TargetUserId, ctx.Reward.Amount, ctx.Reward.Type, ctx.OperateType) return nil } @@ -160,8 +160,8 @@ func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (strin return record.SerialNo, nil } -func updateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus string) error { - _, err := models.UpdateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus) +func UpdateRewardRecordToFinalStatus(sourceType, requestId, newStatus string) error { + _, err := models.UpdateRewardRecordToFinalStatus(sourceType, requestId, newStatus) if err != nil { return err } @@ -169,10 +169,10 @@ func updateAwardOperateRecordStatus(sourceType, requestId, oldStatus, newStatus } func StartPeriodicTaskAsyn(opts *models.StartPeriodicTaskOpts) { - go StartPeriodicTask(opts) + go StartAndGetPeriodicTask(opts) } -func StartPeriodicTask(opts *models.StartPeriodicTaskOpts) error { +func StartAndGetPeriodicTask(opts *models.StartPeriodicTaskOpts) (*models.RewardPeriodicTask, error) { defer func() { if err := recover(); err != nil { combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) @@ -183,35 +183,46 @@ func StartPeriodicTask(opts *models.StartPeriodicTaskOpts) error { var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardOperateLock(opts.RequestId, opts.SourceType.Name(), opts.OperateType.Name())) isOk, err := rewardLock.Lock(3 * time.Second) if err != nil { - return err + return nil, err } if !isOk { log.Info("duplicated operate request,targetUserId=%d requestId=%s", opts.TargetUserId, opts.RequestId) - return nil + return nil, nil } defer rewardLock.UnLock() - //is handled before? - isHandled, err := isHandled(opts.SourceType.Name(), opts.RequestId, opts.OperateType.Name()) - if err != nil { - log.Error("operate is handled error,%v", err) - return err + _, err = models.GetPointOperateRecordBySourceTypeAndRequestId(opts.SourceType.Name(), opts.RequestId, opts.OperateType.Name()) + if err == nil { + task, err := models.GetPeriodicTaskBySourceIdAndType(opts.SourceType, opts.SourceId, opts.OperateType) + if err != nil { + log.Error("GetPeriodicTaskBySourceIdAndType error,%v", err) + return nil, err + } + return task, nil } - if isHandled { - log.Info("operate has been handled,opts=%+v", opts) - return nil + + if err != nil && !models.IsErrRecordNotExist(err) { + log.Error("operate is handled error,%v", err) + return nil, err } + //new reward operate record recordId, err := createPeriodicRewardOperateRecord(opts) if err != nil { - return err + return nil, err } if err = NewRewardPeriodicTask(recordId, opts); err != nil { - updateAwardOperateRecordStatus(opts.SourceType.Name(), opts.RequestId, models.OperateStatusOperating, models.OperateStatusFailed) - return err + UpdateRewardRecordToFinalStatus(opts.SourceType.Name(), opts.RequestId, models.OperateStatusFailed) + return nil, err } - return nil + + task, err := models.GetPeriodicTaskBySourceIdAndType(opts.SourceType, opts.SourceId, opts.OperateType) + if err != nil { + log.Error("GetPeriodicTaskBySourceIdAndType error,%v", err) + return nil, err + } + return task, nil } func StopPeriodicTaskAsyn(sourceType models.SourceType, sourceId string, operateType models.RewardOperateType) { diff --git a/services/reward/period_task.go b/services/reward/period_task.go index 3fa416dab..c2808c4c0 100644 --- a/services/reward/period_task.go +++ b/services/reward/period_task.go @@ -6,6 +6,8 @@ import ( "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/modules/redis/redis_lock" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/routers/repo" + "errors" "fmt" "time" ) @@ -46,33 +48,33 @@ func StartRewardTask() { } } -func RunRewardTask(t models.RewardPeriodicTask, now time.Time) { +func RunRewardTask(t models.RewardPeriodicTask, now time.Time) error { lock := redis_lock.NewDistributeLock(redis_key.RewardTaskRunningLock(t.ID)) isOk, _ := lock.LockWithWait(3*time.Second, 3*time.Second) if !isOk { log.Error("get RewardTaskRunningLock failed,t=%+v", t) - return + return errors.New("get RewardTaskRunningLock failed") } defer lock.UnLock() record, err := models.GetPointOperateRecordBySerialNo(t.OperateSerialNo) if err != nil { log.Error("RunRewardTask. GetPointOperateRecordBySerialNo error. %v", err) - return + return errors.New("GetPointOperateRecordBySerialNo error") } if record.Status != models.OperateStatusOperating { log.Info("RunRewardTask. operate record is finished,record=%+v", record) - return + return nil } n, _ := countExecuteTimes(t, now) if n == 0 { - return + return nil } //get operator operator := GetOperator(models.GetRewardTypeInstance(record.RewardType)) if operator == nil { log.Error("RunRewardTask. operator of reward type is not exist") - return + return errors.New("operator of reward type is not exist") } nextTime := t.NextExecuteTime for i := 0; int64(i) <= n; i++ { @@ -89,14 +91,20 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) { if err != nil { log.Error("RunRewardTask.operator operate error.%v", err) if models.IsErrInsufficientPointsBalance(err) { - StopCloudbrainTask(record) - return + task, err := models.GetCloudbrainByID(record.SourceId) + if err != nil { + log.Error("RunRewardTask GetCloudbrainByID error. %v", err) + return err + } + repo.StopJobs([]*models.Cloudbrain{task}) + return nil } - return + return nil } - models.IncrRewardTaskSuccessCount(t, n, nextTime) + models.IncrRewardTaskSuccessCount(t, 1, nextTime) nextTime = timeutil.TimeStamp(int64(nextTime) + t.IntervalSeconds) } + return nil } @@ -111,8 +119,3 @@ func countExecuteTimes(t models.RewardPeriodicTask, now time.Time) (int64, timeu newNextTime := timeutil.TimeStamp(nextTime + n*interval) return n, newNextTime } - -func StopCloudbrainTask(r *models.RewardOperateRecord) { - //todo - -} diff --git a/services/reward/point/account/point_account.go b/services/reward/point/account/point_account.go index ea127e162..693694c76 100644 --- a/services/reward/point/account/point_account.go +++ b/services/reward/point/account/point_account.go @@ -5,6 +5,7 @@ import ( "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/modules/redis/redis_lock" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "encoding/json" "time" @@ -60,3 +61,20 @@ func InitAccount(userId int64) (*models.PointAccount, error) { return nil, nil } + +//IsPointBalanceEnough check whether the user's point balance is bigger than task unit price +func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int) bool { + if !setting.CloudBrainPaySwitch { + return true + } + spec := models.GetResourceSpec(jobType, resourceSpecId) + if spec == nil { + return true + } + a, error := GetAccount(targetUserId) + if error != nil { + return false + } + return a.Balance >= spec.UnitPrice + +} diff --git a/services/reward/record.go b/services/reward/record.go index 157e53b53..b1ac86876 100644 --- a/services/reward/record.go +++ b/services/reward/record.go @@ -1,9 +1,11 @@ package reward -import "code.gitea.io/gitea/models" +import ( + "code.gitea.io/gitea/models" +) type RecordResponse struct { - Records []models.RewardOperateRecordShow + Records []*models.RewardOperateRecordShow Total int64 PageSize int Page int @@ -14,9 +16,13 @@ func GetRewardRecordList(opts models.RewardRecordListOpts) (*RecordResponse, err if err != nil { return nil, err } - r := make([]models.RewardOperateRecordShow, 0) - for _, v := range l { - r = append(r, v.ToShow()) + if len(l) == 0 { + return &RecordResponse{Records: make([]*models.RewardOperateRecordShow, 0), Total: n, Page: opts.Page, PageSize: opts.PageSize}, nil } - return &RecordResponse{Records: r, Total: n, Page: opts.Page, PageSize: opts.PageSize}, nil + result, err := l.ToShow() + if err != nil { + return nil, err + } + + return &RecordResponse{Records: result, Total: n, Page: opts.Page, PageSize: opts.PageSize}, nil } diff --git a/services/task/task.go b/services/task/task.go index 0dfc38b6c..e5b57ac3d 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -3,7 +3,6 @@ package task import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/reward" "code.gitea.io/gitea/services/reward/limiter" "fmt" @@ -55,9 +54,7 @@ func accomplish(action models.Action) error { } //add log - logId := util.UUID() _, err = models.InsertTaskAccomplishLog(&models.TaskAccomplishLog{ - LogId: logId, ConfigId: config.ID, TaskCode: config.TaskCode, UserId: userId, @@ -70,14 +67,14 @@ func accomplish(action models.Action) error { //reward reward.Operate(&models.RewardOperateContext{ SourceType: models.SourceTypeAccomplishTask, - SourceId: logId, + SourceId: fmt.Sprint(action.ID), Tittle: config.Tittle, Reward: models.Reward{ Amount: config.AwardAmount, Type: models.GetRewardTypeInstance(config.AwardType), }, TargetUserId: userId, - RequestId: logId, + RequestId: fmt.Sprint(action.ID), OperateType: models.OperateTypeIncrease, RejectPolicy: models.FillUp, }) From ec517ba3110078718fe8ca2d002e3c3ecb8f5675 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 28 Jun 2022 18:46:08 +0800 Subject: [PATCH 020/283] #2225 add admin log in reward record show --- models/reward_admin_log.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/models/reward_admin_log.go b/models/reward_admin_log.go index b1a55af13..24e3b8c47 100644 --- a/models/reward_admin_log.go +++ b/models/reward_admin_log.go @@ -2,7 +2,6 @@ package models import ( "code.gitea.io/gitea/modules/timeutil" - "strings" ) const ( @@ -24,6 +23,11 @@ type RewardAdminLog struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` } +type AdminLogAndUser struct { + AdminRewardAdminLog RewardAdminLog `xorm:"extends"` + User User `xorm:"extends"` +} + func getRewardAdminLog(ra *RewardAdminLog) (*RewardAdminLog, error) { has, err := x.Get(ra) if err != nil { @@ -50,12 +54,16 @@ func GetRewardAdminLogByLogIds(logIds []string) ([]RewardAdminLog, error) { if len(logIds) == 0 { return nil, nil } - adminLogs := make([]RewardAdminLog, 0) - err := x.SQL("select rdl.id,rdl.log_id,rdl.amount,rdl.reward_type,rdl.remark,rdl.status,rdl.target_user_id,rdl.creator_id,u.name as creator_name "+ - "from reward_admin_log rdl left join public.user u on rdl.creator_id = u.id "+ - "where rdl.log_id in (?)", strings.Join(logIds, ",")).Find(&adminLogs) + adminLogs := make([]AdminLogAndUser, 0) + err := x.Table("reward_admin_log").Join("LEFT", "user", "reward_admin_log.creator_id = public.user.id").In("reward_admin_log.log_id", logIds).Find(&adminLogs) if err != nil { return nil, err } - return adminLogs, nil + r := make([]RewardAdminLog, len(adminLogs)) + for i, v := range adminLogs { + temp := v.AdminRewardAdminLog + temp.CreatorName = v.User.Name + r[i] = temp + } + return r, nil } From f7430122a57a6853d34b558394f3117bba707ed2 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 29 Jun 2022 12:00:16 +0800 Subject: [PATCH 021/283] #2225 update --- models/action.go | 17 ++++++ models/repo.go | 6 ++ models/reward_operate_record.go | 76 ++++++++++++++------------ models/reward_periodic_task.go | 2 +- models/user.go | 4 ++ services/reward/cloubrain_deduct.go | 53 ++++++++++-------- services/reward/notify.go | 2 +- services/reward/operator.go | 1 + services/reward/period_task.go | 3 +- services/reward/point/point_operate.go | 7 +-- 10 files changed, 104 insertions(+), 67 deletions(-) diff --git a/models/action.go b/models/action.go index ff16dcd3f..69ad797d6 100755 --- a/models/action.go +++ b/models/action.go @@ -91,6 +91,23 @@ type Action struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` } +type ActionShow struct { + UserID int64 + OpType ActionType + ActUserID int64 + ActUser *UserShow + RepoID int64 + Repo *RepositoryShow + CommentID int64 + Comment *Comment `xorm:"-"` + IsDeleted bool `xorm:"INDEX NOT NULL DEFAULT false"` + RefName string + IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"` + IsTransformed bool `xorm:"INDEX NOT NULL DEFAULT false"` + Content string `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + // GetOpType gets the ActionType of this action. func (a *Action) GetOpType() ActionType { return a.OpType diff --git a/models/repo.go b/models/repo.go index db2694617..9845ffa82 100755 --- a/models/repo.go +++ b/models/repo.go @@ -237,6 +237,12 @@ type Repository struct { LowerAlias string `xorm:"INDEX"` } +type RepositoryShow struct { + Name string + RepoType RepoType + Alias string +} + // SanitizedOriginalURL returns a sanitized OriginalURL func (repo *Repository) SanitizedOriginalURL() string { if repo.OriginalURL == "" { diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 04f43a8bd..9c6b347a6 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -213,21 +213,22 @@ func (l *RewardRecordList) GetRewardRecordCloudbrainTask() (map[string]Cloudbrai } type RewardOperateRecord struct { - ID int64 `xorm:"pk autoincr"` - SerialNo string `xorm:"INDEX NOT NULL"` - UserId int64 `xorm:"INDEX NOT NULL"` - Amount int64 `xorm:"NOT NULL"` - Tittle string - RewardType string `xorm:"NOT NULL"` - SourceType string `xorm:"NOT NULL"` - SourceId string `xorm:"INDEX NOT NULL"` - RequestId string `xorm:"INDEX NOT NULL"` - OperateType string `xorm:"NOT NULL"` - Status string `xorm:"NOT NULL"` - Remark string - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - FinishedUnix timeutil.TimeStamp `xorm:"INDEX"` + ID int64 `xorm:"pk autoincr"` + SerialNo string `xorm:"INDEX NOT NULL"` + UserId int64 `xorm:"INDEX NOT NULL"` + Amount int64 `xorm:"NOT NULL"` + LossAmount int64 + Tittle string + RewardType string `xorm:"NOT NULL"` + SourceType string `xorm:"NOT NULL"` + SourceId string `xorm:"INDEX NOT NULL"` + RequestId string `xorm:"INDEX NOT NULL"` + OperateType string `xorm:"NOT NULL"` + Status string `xorm:"NOT NULL"` + Remark string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + LastOperateUnix timeutil.TimeStamp `xorm:"INDEX"` } type AdminRewardOperateReq struct { @@ -240,27 +241,31 @@ type AdminRewardOperateReq struct { func (r RewardOperateRecord) ToShow() RewardOperateRecordShow { return RewardOperateRecordShow{ - SerialNo: r.SerialNo, - Date: r.CreatedUnix, - OperateType: r.OperateType, - Amount: r.Amount, - Remark: r.Remark, - Status: r.Status, - SourceType: r.SourceType, + SerialNo: r.SerialNo, + Date: r.CreatedUnix, + OperateType: r.OperateType, + Amount: r.Amount, + Remark: r.Remark, + Status: r.Status, + SourceType: r.SourceType, + LastOperateTask: r.LastOperateUnix, + LossAmount: r.LossAmount, } } type RewardOperateRecordShow struct { - SerialNo string - Date timeutil.TimeStamp - Status string - OperateType string - Amount int64 - Remark string - SourceType string - Action Action - Cloudbrain Cloudbrain - AdminLog RewardAdminLog + SerialNo string + Date timeutil.TimeStamp + Status string + OperateType string + Amount int64 + LossAmount int64 + Remark string + SourceType string + LastOperateTask timeutil.TimeStamp + Action Action + Cloudbrain Cloudbrain + AdminLog RewardAdminLog } func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error) { @@ -295,10 +300,10 @@ func InsertRewardOperateRecord(tl *RewardOperateRecord) (int64, error) { func UpdateRewardRecordToFinalStatus(sourceType, requestId, newStatus string) (int64, error) { r := &RewardOperateRecord{ - Status: newStatus, - FinishedUnix: timeutil.TimeStampNow(), + Status: newStatus, + LastOperateUnix: timeutil.TimeStampNow(), } - return x.Cols("status", "finished_unix").Where("source_type=? and request_id=? and status=?", sourceType, requestId, OperateStatusOperating).Update(r) + return x.Cols("status", "last_operate_unix").Where("source_type=? and request_id=? and status=?", sourceType, requestId, OperateStatusOperating).Update(r) } func SumRewardAmountInTaskPeriod(rewardType string, sourceType string, userId int64, period *PeriodResult) (int64, error) { @@ -326,6 +331,7 @@ type RewardOperateContext struct { OperateType RewardOperateType RejectPolicy LimiterRejectPolicy PermittedNegative bool + LossAmount int64 } type Reward struct { diff --git a/models/reward_periodic_task.go b/models/reward_periodic_task.go index a859676d6..910f4fe8e 100644 --- a/models/reward_periodic_task.go +++ b/models/reward_periodic_task.go @@ -77,7 +77,7 @@ func IncrRewardTaskSuccessCount(t RewardPeriodicTask, count int64, nextTime time sess.Rollback() return err } - _, err = sess.Exec("update reward_operate_record set amount = amount + ? ,updated_unix = ? where serial_no = ?", t.Amount, timeutil.TimeStampNow(), t.OperateSerialNo) + _, err = sess.Exec("update reward_operate_record set amount = amount + ? ,updated_unix = ? ,last_operate_unix = ? where serial_no = ?", t.Amount, timeutil.TimeStampNow(), timeutil.TimeStampNow(), t.OperateSerialNo) if err != nil { sess.Rollback() return err diff --git a/models/user.go b/models/user.go index dd5a6f1d2..73537556a 100755 --- a/models/user.go +++ b/models/user.go @@ -186,6 +186,10 @@ type User struct { WechatBindUnix timeutil.TimeStamp } +type UserShow struct { + Name string +} + // SearchOrganizationsOptions options to filter organizations type SearchOrganizationsOptions struct { ListOptions diff --git a/services/reward/cloubrain_deduct.go b/services/reward/cloubrain_deduct.go index 1e547a8a1..39354b98c 100644 --- a/services/reward/cloubrain_deduct.go +++ b/services/reward/cloubrain_deduct.go @@ -59,7 +59,7 @@ func StartCloudbrainPointDeductTask() { }() log.Debug("try to run CloudbrainPointDeductTask") end := time.Now() - start := end.Add(-5 * time.Minute) + start := end.Add(-30 * time.Minute) if firstTimeFlag { //When it is executed for the first time, it needs to process the tasks of the last 1 hours. //This is done to prevent the application from hanging for a long time @@ -76,30 +76,35 @@ func StartCloudbrainPointDeductTask() { return } for _, t := range taskList { - //初始化 period_task 和 operate_record - if int64(t.StartTime) > end.Unix() || int64(t.StartTime) < start.Unix() { - continue - } + DeductPoint4Cloudbrain(t, end) + } +} - task, err := StartAndGetCloudBrainPointDeductTask(t) - if err != nil { - log.Error("run cloubrain point deduct task error,err=%v", err) - continue - } - if task == nil { - continue - } - if task.Status == models.PeriodicTaskStatusFinished { - log.Info("Periodic task is finished") - continue - } +func DeductPoint4Cloudbrain(t models.Cloudbrain, now time.Time) error { - if int64(t.EndTime) <= end.Unix() && int64(t.EndTime) >= start.Unix() { - endTime := time.Unix(int64(t.EndTime), 0) - RunRewardTask(*task, endTime) - models.StopPeriodicTask(task.ID, task.OperateSerialNo, endTime) - } else { - RunRewardTask(*task, end) - } + if t.StartTime == 0 { + return nil + } + + task, err := StartAndGetCloudBrainPointDeductTask(t) + if err != nil { + log.Error("run cloudbrain point deduct task error,err=%v", err) + return err + } + if task == nil { + return nil + } + if task.Status == models.PeriodicTaskStatusFinished { + log.Info("Periodic task is finished") + return nil + } + + if t.EndTime > 0 { + endTime := time.Unix(int64(t.EndTime), 0) + RunRewardTask(*task, endTime) + models.StopPeriodicTask(task.ID, task.OperateSerialNo, endTime) + } else { + RunRewardTask(*task, now) } + return nil } diff --git a/services/reward/notify.go b/services/reward/notify.go index f5b270d94..5cfe6ee77 100644 --- a/services/reward/notify.go +++ b/services/reward/notify.go @@ -35,7 +35,7 @@ func GetRewardOperation(since, until timeutil.TimeStamp) []models.UserRewardOper json.Unmarshal([]byte(v), &t) r = append(r, models.UserRewardOperation{ UserId: t.UserId, - Msg: GetRewardOperateMsg(t), + Msg: v, }) } redis_client.ZRemRangeByScore(redis_key.RewardOperateNotification(), float64(since), float64(until)) diff --git a/services/reward/operator.go b/services/reward/operator.go index fc51aa1c5..8a3ea12f2 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -118,6 +118,7 @@ func initRewardOperateRecord(ctx *models.RewardOperateContext) (string, error) { record := &models.RewardOperateRecord{ UserId: ctx.TargetUserId, Amount: ctx.Reward.Amount, + LossAmount: ctx.LossAmount, RewardType: ctx.Reward.Type.Name(), SourceType: ctx.SourceType.Name(), SourceId: ctx.SourceId, diff --git a/services/reward/period_task.go b/services/reward/period_task.go index c2808c4c0..f6bd45fc9 100644 --- a/services/reward/period_task.go +++ b/services/reward/period_task.go @@ -77,7 +77,7 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) error { return errors.New("operator of reward type is not exist") } nextTime := t.NextExecuteTime - for i := 0; int64(i) <= n; i++ { + for i := 1; int64(i) <= n; i++ { err = operator.Operate(&models.RewardOperateContext{ SourceType: models.SourceTypeRunCloudbrainTask, SourceId: t.OperateSerialNo, @@ -97,6 +97,7 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) error { return err } repo.StopJobs([]*models.Cloudbrain{task}) + models.StopPeriodicTask(task.ID, t.OperateSerialNo, time.Now()) return nil } return nil diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 0115c288a..101758db8 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -9,12 +9,9 @@ import ( "code.gitea.io/gitea/services/reward/limiter" "code.gitea.io/gitea/services/reward/point/account" "errors" - "fmt" "time" ) -const LossMsg = "达到奖励上限,应得%d积分,实得%d积分" - type PointOperator struct { } @@ -24,7 +21,7 @@ func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) error return err } if realAmount < ctx.Reward.Amount { - ctx.Remark = models.AppendRemark(ctx.Remark, fmt.Sprintf(LossMsg, ctx.Reward.Amount, realAmount)) + ctx.LossAmount = ctx.Reward.Amount - realAmount ctx.Reward.Amount = realAmount } return nil @@ -49,7 +46,7 @@ func (operator *PointOperator) Operate(ctx *models.RewardOperateContext) error { } else if ctx.OperateType == models.OperateTypeDecrease { if !ctx.PermittedNegative && na.Balance < ctx.Reward.Amount { log.Info("account balance is not enough,ctx=%v", ctx) - return &models.ErrInsufficientPointsBalance{} + return models.ErrInsufficientPointsBalance{} } err = na.Decrease(ctx.Reward.Amount, ctx.SourceId) } From 77e9692c2e13781810b2f54b8ded58f678d3bb30 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 29 Jun 2022 15:07:22 +0800 Subject: [PATCH 022/283] #2225 update --- routers/routes/routes.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 47ee3c50a..6cf87b527 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -594,6 +594,16 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/empty", admin.EmptyNotices) }) + }, adminReq) + // ***** END: Admin ***** + + operationReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, OperationRequired: true}) + + // ***** START: Operation ***** + m.Group("/operation", func() { + m.Get("/config/recommend_org", operation.Organizations) + m.Post("/config/recommend_org", bindIgnErr(operation.OrgInfos{}), operation.UpdateRecommendOrganizations) + m.Group("/reward/point", func() { m.Get("/limiter/list", point.GetPointLimitConfigList) m.Post("/limiter/add", bindIgnErr(models.LimitConfigVO{}), point.AddPointLimitConfig) @@ -606,16 +616,6 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/add", bindIgnErr(models.TaskConfigWithLimit{}), task.AddTaskConfig) m.Post("/add/batch", bindIgnErr(models.BatchLimitConfigVO{}), task.BatchAddTaskConfig) }) - - }, adminReq) - // ***** END: Admin ***** - - operationReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, OperationRequired: true}) - - // ***** START: Operation ***** - m.Group("/operation", func() { - m.Get("/config/recommend_org", operation.Organizations) - m.Post("/config/recommend_org", bindIgnErr(operation.OrgInfos{}), operation.UpdateRecommendOrganizations) }, operationReq) // ***** END: Operation ***** From 0efaa9b16c131b13a6b1d7346207d3ebc6345155 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 29 Jun 2022 15:40:49 +0800 Subject: [PATCH 023/283] #2225 add log --- services/reward/cloubrain_deduct.go | 1 + services/reward/notify.go | 2 ++ services/reward/operator.go | 15 +++++++++++++++ services/reward/period_task.go | 1 + services/reward/point/account/point_account.go | 9 +++++++-- services/reward/point/point_operate.go | 5 +++++ services/reward/record.go | 4 ++++ services/reward/serial.go | 2 ++ services/task/task.go | 4 +++- services/task/task_config.go | 4 ++++ 10 files changed, 44 insertions(+), 3 deletions(-) diff --git a/services/reward/cloubrain_deduct.go b/services/reward/cloubrain_deduct.go index 39354b98c..7d0c39028 100644 --- a/services/reward/cloubrain_deduct.go +++ b/services/reward/cloubrain_deduct.go @@ -22,6 +22,7 @@ func StartAndGetCloudBrainPointDeductTask(task models.Cloudbrain) (*models.Rewar spec := models.GetResourceSpec(task.JobType, task.ResourceSpecId) if spec == nil || spec.UnitPrice == 0 { + log.Debug("GetResourceSpec failed,spec is nil or UnitPrice = 0") return nil, nil } diff --git a/services/reward/notify.go b/services/reward/notify.go index 5cfe6ee77..2cd27f007 100644 --- a/services/reward/notify.go +++ b/services/reward/notify.go @@ -2,6 +2,7 @@ package reward import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/modules/timeutil" @@ -24,6 +25,7 @@ func NotifyRewardOperation(userId, amount int64, rewardType models.RewardType, o func GetRewardOperation(since, until timeutil.TimeStamp) []models.UserRewardOperation { list, err := redis_client.ZRangeByScore(redis_key.RewardOperateNotification(), float64(since), float64(until)) if err != nil { + log.Error("GetRewardOperation ZRangeByScore error. %v", err) return nil } if len(list) == 0 { diff --git a/services/reward/operator.go b/services/reward/operator.go index 8a3ea12f2..3a869d772 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -57,6 +57,7 @@ func Operate(ctx *models.RewardOperateContext) error { //get operator operator := GetOperator(ctx.Reward.Type) if operator == nil { + log.Error("operator of reward type is not exist,ctx=%v", ctx) return errors.New("operator of reward type is not exist") } @@ -71,6 +72,7 @@ func Operate(ctx *models.RewardOperateContext) error { //new reward operate record recordId, err := initRewardOperateRecord(ctx) if err != nil { + log.Error("initRewardOperateRecord error,err=%v", err) return err } @@ -78,6 +80,7 @@ func Operate(ctx *models.RewardOperateContext) error { //operate if err := operator.Operate(ctx); err != nil { + log.Error("operator Operate error,err=%v", err) UpdateRewardRecordToFinalStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusFailed) return err } @@ -101,9 +104,11 @@ func GetOperator(rewardType models.RewardType) RewardOperator { func isHandled(sourceType string, requestId string, operateType string) (bool, error) { _, err := models.GetPointOperateRecordBySourceTypeAndRequestId(sourceType, requestId, operateType) if err != nil { + log.Error("operator isHandled error. %v", err) if models.IsErrRecordNotExist(err) { return false, nil } + log.Error("GetPointOperateRecordBySourceTypeAndRequestId ZRangeByScore error. %v", err) return false, err } return true, nil @@ -113,6 +118,7 @@ func isHandled(sourceType string, requestId string, operateType string) (bool, e func initRewardOperateRecord(ctx *models.RewardOperateContext) (string, error) { sn, err := generateOperateSerialNo(ctx.OperateType, ctx.Reward.Type) if err != nil { + log.Error("generateOperateSerialNo error. %v", err) return "", err } record := &models.RewardOperateRecord{ @@ -131,6 +137,7 @@ func initRewardOperateRecord(ctx *models.RewardOperateContext) (string, error) { } _, err = models.InsertRewardOperateRecord(record) if err != nil { + log.Error("InsertRewardOperateRecord error. %v", err) return "", err } return record.SerialNo, nil @@ -139,6 +146,7 @@ func initRewardOperateRecord(ctx *models.RewardOperateContext) (string, error) { func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (string, error) { sn, err := generateOperateSerialNo(ctx.OperateType, ctx.RewardType) if err != nil { + log.Error("createPeriodic generateOperateSerialNo error. %v", err) return "", err } record := &models.RewardOperateRecord{ @@ -156,6 +164,7 @@ func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (strin } _, err = models.InsertRewardOperateRecord(record) if err != nil { + log.Error("createPeriodic InsertRewardOperateRecord error. %v", err) return "", err } return record.SerialNo, nil @@ -164,6 +173,7 @@ func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (strin func UpdateRewardRecordToFinalStatus(sourceType, requestId, newStatus string) error { _, err := models.UpdateRewardRecordToFinalStatus(sourceType, requestId, newStatus) if err != nil { + log.Error("UpdateRewardRecord UpdateRewardRecordToFinalStatus error. %v", err) return err } return nil @@ -184,6 +194,7 @@ func StartAndGetPeriodicTask(opts *models.StartPeriodicTaskOpts) (*models.Reward var rewardLock = redis_lock.NewDistributeLock(redis_key.RewardOperateLock(opts.RequestId, opts.SourceType.Name(), opts.OperateType.Name())) isOk, err := rewardLock.Lock(3 * time.Second) if err != nil { + log.Error("StartAndGetPeriodicTask RewardOperateLock error. %v", err) return nil, err } if !isOk { @@ -210,10 +221,12 @@ func StartAndGetPeriodicTask(opts *models.StartPeriodicTaskOpts) (*models.Reward //new reward operate record recordId, err := createPeriodicRewardOperateRecord(opts) if err != nil { + log.Error("StartAndGetPeriodicTask createPeriodicRewardOperateRecord error. %v", err) return nil, err } if err = NewRewardPeriodicTask(recordId, opts); err != nil { + log.Error("StartAndGetPeriodicTask NewRewardPeriodicTask error. %v", err) UpdateRewardRecordToFinalStatus(opts.SourceType.Name(), opts.RequestId, models.OperateStatusFailed) return nil, err } @@ -258,6 +271,8 @@ func StopPeriodicTask(sourceType models.SourceType, sourceId string, operateType func generateOperateSerialNo(operateType models.RewardOperateType, rewardType models.RewardType) (string, error) { s, err := GetSerialNoByRedis() if err != nil { + log.Error("generateOperateSerialNo error. %v", err) + return "", err } diff --git a/services/reward/period_task.go b/services/reward/period_task.go index f6bd45fc9..b6315b19f 100644 --- a/services/reward/period_task.go +++ b/services/reward/period_task.go @@ -67,6 +67,7 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) error { } n, _ := countExecuteTimes(t, now) if n == 0 { + log.Info("countExecuteTimes result is 0") return nil } diff --git a/services/reward/point/account/point_account.go b/services/reward/point/account/point_account.go index 693694c76..c1d5722c4 100644 --- a/services/reward/point/account/point_account.go +++ b/services/reward/point/account/point_account.go @@ -2,6 +2,7 @@ package account import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/modules/redis/redis_lock" @@ -24,10 +25,12 @@ func GetAccount(userId int64) (*models.PointAccount, error) { if models.IsErrRecordNotExist(err) { a, err := InitAccount(userId) if err != nil { + log.Error("InitAccount error,err=%v", err) return nil, err } return a, nil } + log.Error("GetAccountByUserId error,err=%v", err) return nil, err } jsonStr, _ := json.Marshal(account) @@ -39,6 +42,7 @@ func InitAccount(userId int64) (*models.PointAccount, error) { lock := redis_lock.NewDistributeLock(redis_key.PointAccountInitLock(userId)) isOk, err := lock.LockWithWait(3*time.Second, 3*time.Second) if err != nil { + log.Error("PointAccountInitLock error,err=%v", err) return nil, err } if isOk { @@ -71,8 +75,9 @@ func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int if spec == nil { return true } - a, error := GetAccount(targetUserId) - if error != nil { + a, err := GetAccount(targetUserId) + if err != nil { + log.Error("IsPointBalanceEnough GetAccount error,err=%v", err) return false } return a.Balance >= spec.UnitPrice diff --git a/services/reward/point/point_operate.go b/services/reward/point/point_operate.go index 101758db8..ccdf1f423 100644 --- a/services/reward/point/point_operate.go +++ b/services/reward/point/point_operate.go @@ -18,6 +18,7 @@ type PointOperator struct { func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) error { realAmount, err := limiter.CheckLimit(ctx.SourceType.Name(), models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount, ctx.RejectPolicy) if err != nil { + log.Error("PointOperator IsLimited error,err=%v", err) return err } if realAmount < ctx.Reward.Amount { @@ -30,12 +31,14 @@ func (operator *PointOperator) IsLimited(ctx *models.RewardOperateContext) error func (operator *PointOperator) Operate(ctx *models.RewardOperateContext) error { a, err := account.GetAccount(ctx.TargetUserId) if err != nil || a == nil { + log.Error("operator get account error error,err=%v", err) return errors.New("get account error") } lock := redis_lock.NewDistributeLock(redis_key.PointAccountOperateLock(a.AccountCode)) isOk, err := lock.LockWithWait(3*time.Second, 3*time.Second) if err != nil { + log.Error("Get PointAccountOperateLock error,err=%v", err) return err } if isOk { @@ -51,11 +54,13 @@ func (operator *PointOperator) Operate(ctx *models.RewardOperateContext) error { err = na.Decrease(ctx.Reward.Amount, ctx.SourceId) } if err != nil { + log.Error("operate account balance error,err=%v", err) return err } redis_client.Del(redis_key.PointAccountInfo(ctx.TargetUserId)) } else { + log.Error("Get account operate lock failed,ctx=%v", ctx) return errors.New("Get account operate lock failed") } return nil diff --git a/services/reward/record.go b/services/reward/record.go index b1ac86876..460a6fc81 100644 --- a/services/reward/record.go +++ b/services/reward/record.go @@ -2,6 +2,7 @@ package reward import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" ) type RecordResponse struct { @@ -14,6 +15,8 @@ type RecordResponse struct { func GetRewardRecordList(opts models.RewardRecordListOpts) (*RecordResponse, error) { l, n, err := models.GetRewardRecordList(opts) if err != nil { + log.Error("GetRewardRecordList error. %v", err) + return nil, err } if len(l) == 0 { @@ -21,6 +24,7 @@ func GetRewardRecordList(opts models.RewardRecordListOpts) (*RecordResponse, err } result, err := l.ToShow() if err != nil { + log.Error("GetRewardRecordList ToShow error. %v", err) return nil, err } diff --git a/services/reward/serial.go b/services/reward/serial.go index e9509c403..b6a47bbc3 100644 --- a/services/reward/serial.go +++ b/services/reward/serial.go @@ -1,6 +1,7 @@ package reward import ( + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "fmt" @@ -12,6 +13,7 @@ func GetSerialNoByRedis() (string, error) { now := time.Now() n, err := redis_client.IncrBy(redis_key.RewardSerialCounter(now), 1) if err != nil { + log.Error("GetSerialNoByRedis RewardSerialCounter error. %v", err) return "", err } if n == 1 { diff --git a/services/task/task.go b/services/task/task.go index e5b57ac3d..dcf7007c6 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -61,6 +61,7 @@ func accomplish(action models.Action) error { ActionId: action.ID, }) if err != nil { + log.Error("InsertTaskAccomplishLog error,%v", err) return err } @@ -78,12 +79,13 @@ func accomplish(action models.Action) error { OperateType: models.OperateTypeIncrease, RejectPolicy: models.FillUp, }) - + log.Debug("accomplish success,action=%v", action) return nil } func isLimited(userId int64, config *models.TaskConfig, rejectPolicy models.LimiterRejectPolicy) bool { if _, err := limiter.CheckLimit(config.TaskCode, models.LimitTypeTask, userId, 1, rejectPolicy); err != nil { + log.Error(" isLimited CheckLimit error. %v", err) return true } return false diff --git a/services/task/task_config.go b/services/task/task_config.go index 0184ca15b..0404a6f06 100644 --- a/services/task/task_config.go +++ b/services/task/task_config.go @@ -16,6 +16,7 @@ import ( func GetTaskConfig(taskType string) (*models.TaskConfig, error) { list, err := GetTaskConfigList() if err != nil { + log.Error(" GetTaskConfigList error. %v", err) return nil, err } for _, v := range list { @@ -39,6 +40,7 @@ func GetTaskConfigList() ([]*models.TaskConfig, error) { } config, err := models.GetTaskConfigList() if err != nil { + log.Error(" GetTaskConfigList from model error. %v", err) if models.IsErrRecordNotExist(err) { redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) return nil, nil @@ -61,6 +63,7 @@ func GetTaskConfigWithLimitList() ([]*models.TaskConfigWithLimit, error) { r := make([]*models.TaskConfigWithLimit, 0) l, err := limiter.GetLimitersByLimitType(models.LimitTypeTask) if err != nil { + log.Error(" GetLimitersByLimitType from redis error. %v", err) return nil, err } for i := 0; i < len(list); i++ { @@ -88,6 +91,7 @@ func GetTaskConfigWithLimitList() ([]*models.TaskConfigWithLimit, error) { func AddTaskConfig(config models.TaskConfigWithLimit, doer *models.User) error { if config.TaskCode == "" || config.AwardType == "" { + log.Error(" AddTaskConfig param error") return errors.New("param error") } err := models.AddTaskConfig(config, doer) From 34bdcf070b0fa373aefce51a4b5d0abac0f5b3e1 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 29 Jun 2022 15:53:02 +0800 Subject: [PATCH 024/283] #2225 add log --- modules/eventsource/manager_run.go | 2 ++ services/reward/notify.go | 1 + 2 files changed, 3 insertions(+) diff --git a/modules/eventsource/manager_run.go b/modules/eventsource/manager_run.go index 857eaee22..252a0ec88 100644 --- a/modules/eventsource/manager_run.go +++ b/modules/eventsource/manager_run.go @@ -31,9 +31,11 @@ loop: for { select { case <-rewardTimer.C: + log.Debug("rewardTimer run") now := timeutil.TimeStampNow().Add(-2) list := reward.GetRewardOperation(rewardThen, now) if list != nil { + log.Debug("GetRewardOperation list=%v", list) for _, l := range list { m.SendMessage(l.UserId, &Event{ Name: "reward-operation", diff --git a/services/reward/notify.go b/services/reward/notify.go index 2cd27f007..875dde199 100644 --- a/services/reward/notify.go +++ b/services/reward/notify.go @@ -29,6 +29,7 @@ func GetRewardOperation(since, until timeutil.TimeStamp) []models.UserRewardOper return nil } if len(list) == 0 { + log.Debug("GetRewardOperation list length = 0") return nil } r := make([]models.UserRewardOperation, len(list)) From 6410dba541e43a72b0af8d7f3497de67a8c9aa88 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 29 Jun 2022 17:01:46 +0800 Subject: [PATCH 025/283] #2225 update --- modules/redis/redis_key/reward_redis_key.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/redis/redis_key/reward_redis_key.go b/modules/redis/redis_key/reward_redis_key.go index 05c10ce4f..fb294668a 100644 --- a/modules/redis/redis_key/reward_redis_key.go +++ b/modules/redis/redis_key/reward_redis_key.go @@ -1,7 +1,9 @@ package redis_key import ( + "code.gitea.io/gitea/modules/setting" "fmt" + "strings" ) const REWARD_REDIS_PREFIX = "reward" @@ -11,7 +13,7 @@ func RewardOperateLock(requestId string, sourceType string, operateType string) } func RewardOperateNotification() string { - return KeyJoin(REWARD_REDIS_PREFIX, "operate", "notification") + return KeyJoin(REWARD_REDIS_PREFIX, "operate", strings.ReplaceAll(setting.AppURL, "/", ""), "notification") } func RewardTaskRunningLock(taskId int64) string { From 2fabac1b536f838139e2a5b9c08dbb05009d5bab Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 29 Jun 2022 17:26:41 +0800 Subject: [PATCH 026/283] #2225 update --- models/reward_operate_record.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 9c6b347a6..444e477b6 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -242,27 +242,27 @@ type AdminRewardOperateReq struct { func (r RewardOperateRecord) ToShow() RewardOperateRecordShow { return RewardOperateRecordShow{ SerialNo: r.SerialNo, - Date: r.CreatedUnix, + CreateDate: r.CreatedUnix, OperateType: r.OperateType, Amount: r.Amount, Remark: r.Remark, Status: r.Status, SourceType: r.SourceType, - LastOperateTask: r.LastOperateUnix, + LastOperateDate: r.LastOperateUnix, LossAmount: r.LossAmount, } } type RewardOperateRecordShow struct { SerialNo string - Date timeutil.TimeStamp + CreateDate timeutil.TimeStamp Status string OperateType string Amount int64 LossAmount int64 Remark string SourceType string - LastOperateTask timeutil.TimeStamp + LastOperateDate timeutil.TimeStamp Action Action Cloudbrain Cloudbrain AdminLog RewardAdminLog From 186e6d1bd38d11e29ecee840a9f127300ccf1189 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 30 Jun 2022 12:00:59 +0800 Subject: [PATCH 027/283] #2225 update --- routers/reward/point/point.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index fa5e31afa..d912c1539 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -10,7 +10,7 @@ import ( "net/http" ) -const tplPoint base.TplName = "/reward/point" +const tplPoint base.TplName = "reward/point" type AccountResponse struct { Balance int64 From 39339a29e6a7791062d208c70a43d6eece269ffc Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 30 Jun 2022 16:08:59 +0800 Subject: [PATCH 028/283] #2225 update --- models/action.go | 61 +++-- models/action_list.go | 65 ++++++ models/cloudbrain.go | 211 +++++++++++------- models/helper.go | 8 + models/reward_admin_log.go | 8 +- models/reward_operate_record.go | 29 +-- modules/auth/modelarts.go | 3 - modules/modelarts/modelarts.go | 5 +- routers/repo/cloudbrain.go | 8 +- routers/repo/modelarts.go | 19 +- services/reward/cloubrain_deduct.go | 8 +- .../reward/point/account/point_account.go | 14 +- services/task/task.go | 8 +- 13 files changed, 295 insertions(+), 152 deletions(-) diff --git a/models/action.go b/models/action.go index 69ad797d6..33322a921 100755 --- a/models/action.go +++ b/models/action.go @@ -92,20 +92,14 @@ type Action struct { } type ActionShow struct { - UserID int64 - OpType ActionType - ActUserID int64 - ActUser *UserShow - RepoID int64 - Repo *RepositoryShow - CommentID int64 - Comment *Comment `xorm:"-"` - IsDeleted bool `xorm:"INDEX NOT NULL DEFAULT false"` - RefName string - IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"` - IsTransformed bool `xorm:"INDEX NOT NULL DEFAULT false"` - Content string `xorm:"TEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + OpType ActionType + RepoLink string + ShortRepoFullDisplayName string + Content string + RefName string + IssueInfos []string + CommentLink string + IssueTitle string } // GetOpType gets the ActionType of this action. @@ -243,6 +237,43 @@ func (a *Action) GetRepoLink() string { return "/" + a.GetRepoPath() } +func (a *Action) ToShow() *ActionShow { + actionShow := &ActionShow{} + actionShow.OpType = GetTaskOptType(*a) + actionShow.Content = a.Content + actionShow.RefName = a.RefName + + if strings.Contains(a.Content, "|") { + actionShow.IssueInfos = a.GetIssueInfos() + actionShow.IssueTitle = a.GetIssueTitle() + } + + if a.Repo != nil { + actionShow.RepoLink = a.GetRepoLink() + actionShow.ShortRepoFullDisplayName = a.ShortRepoFullDisplayName() + } + if a.Comment != nil { + actionShow.CommentLink = a.GetCommentLink() + } + + return actionShow +} + +func GetTaskOptType(action Action) ActionType { + switch action.OpType { + case ActionCreateDebugGPUTask, + ActionCreateDebugNPUTask, + ActionCreateTrainTask, + ActionCreateInferenceTask, + ActionCreateBenchMarkTask, + ActionCreateGPUTrainTask: + return ActionCreateCloudbrainTask + default: + return action.OpType + } + +} + // GetRepositoryFromMatch returns a *Repository from a username and repo strings func GetRepositoryFromMatch(ownerName string, repoName string) (*Repository, error) { var err error @@ -439,7 +470,7 @@ func GetActionByIds(ids []int64) ([]*Action, error) { if err != nil { return nil, err } - if err := ActionList(actions).LoadAttributes(); err != nil { + if err := ActionList(actions).LoadAllAttributes(); err != nil { return nil, fmt.Errorf("ActionList loadAttributes: %v", err) } return actions, nil diff --git a/models/action_list.go b/models/action_list.go index 6f726f4b3..a0987c20d 100644 --- a/models/action_list.go +++ b/models/action_list.go @@ -79,6 +79,48 @@ func (actions ActionList) LoadRepositories() ([]*Repository, error) { return actions.loadRepositories(x) } +func (actions ActionList) getCommentIDs() []int64 { + commentIDs := make(map[int64]struct{}, len(actions)) + for _, action := range actions { + if action.CommentID == 0 { + continue + } + if _, ok := commentIDs[action.CommentID]; !ok { + commentIDs[action.CommentID] = struct{}{} + } + } + return keysInt64(commentIDs) +} + +func (actions ActionList) loadComments(e Engine) ([]*Comment, error) { + if len(actions) == 0 { + return nil, nil + } + + commentIDs := actions.getCommentIDs() + + commentMaps := make(map[int64]*Comment, len(commentIDs)) + if len(commentIDs) == 0 { + return make([]*Comment, 0), nil + } + err := e. + In("id", commentIDs). + Find(&commentMaps) + if err != nil { + return nil, fmt.Errorf("find comment: %v", err) + } + + for _, action := range actions { + action.Comment = commentMaps[action.CommentID] + } + return valuesComment(commentMaps), nil +} + +// LoadComments loads actions' all comments +func (actions ActionList) LoadComments() ([]*Comment, error) { + return actions.loadComments(x) +} + // loadAttributes loads all attributes func (actions ActionList) loadAttributes(e Engine) (err error) { if _, err = actions.loadUsers(e); err != nil { @@ -96,3 +138,26 @@ func (actions ActionList) loadAttributes(e Engine) (err error) { func (actions ActionList) LoadAttributes() error { return actions.loadAttributes(x) } + +// LoadAllAttributes loads all attributes of the actions +// compare with LoadAttributes() ,LoadAllAttributes() loads Comment attribute +func (actions ActionList) LoadAllAttributes() error { + return actions.loadAllAttributes(x) +} + +// loadAllAttributes +func (actions ActionList) loadAllAttributes(e Engine) (err error) { + if _, err = actions.loadUsers(e); err != nil { + return + } + + if _, err = actions.loadRepositories(e); err != nil { + return + } + + if _, err = actions.loadComments(e); err != nil { + return + } + + return nil +} diff --git a/models/cloudbrain.go b/models/cloudbrain.go index 06cd42258..2f82640b7 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -169,69 +169,23 @@ type Cloudbrain struct { } type CloudbrainShow struct { - JobID string `xorm:"INDEX NOT NULL"` - JobType string `xorm:"INDEX NOT NULL DEFAULT 'DEBUG'"` - JobName string - DisplayJobName string - Status string - UserID int64 `xorm:"INDEX NOT NULL"` - RepoID int64 `xorm:"INDEX NOT NULL"` - SubTaskName string - ContainerID string - ContainerIp string - CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - Duration int64 `xorm:"DEFAULT 0"` //运行时长 单位秒 - TrainJobDuration string `xorm:"DEFAULT '00:00:00'"` - Image string //镜像名称 - GpuQueue string //GPU类型即GPU队列 - ResourceSpecId int //GPU规格id - DeletedAt time.Time `xorm:"deleted"` - CanDebug bool `xorm:"-"` - CanDel bool `xorm:"-"` - CanModify bool `xorm:"-"` - Type int - BenchmarkTypeID int - BenchmarkChildTypeID int - - VersionID int64 //版本id - VersionName string `xorm:"INDEX"` //当前版本 - Uuid string //数据集id - DatasetName string - VersionCount int //任务的当前版本数量,不包括删除的 - IsLatestVersion string //是否是最新版本,1是,0否 - CommitID string //提交的仓库代码id - PreVersionName string //父版本名称 - ComputeResource string //计算资源,例如npu - EngineID int64 //引擎id - - TrainUrl string //输出模型的obs路径 - BranchName string //分支名称 - Parameters string //传给modelarts的param参数 - BootFile string //启动文件 - DataUrl string //数据集的obs路径 - LogUrl string //日志输出的obs路径 - PreVersionId int64 //父版本的版本id - FlavorCode string //modelarts上的规格id - Description string `xorm:"varchar(256)"` //描述 - WorkServerNumber int //节点数 - FlavorName string //规格名称 - EngineName string //引擎名称 - TotalVersionCount int //任务的所有版本数量,包括删除的 - - LabelName string //标签名称 - ModelName string //模型名称 - ModelVersion string //模型版本 - CkptName string //权重文件名称 - ResultUrl string //推理结果的obs路径 + ID int64 + JobType string + DisplayJobName string + Duration string + ResourceSpec *ResourceAndFlavor + ComputeResource string +} - User *User `xorm:"-"` - Repo *Repository `xorm:"-"` - BenchmarkType string `xorm:"-"` //算法评测,模型评测 - BenchmarkTypeName string `xorm:"-"` - BenchmarkTypeRankLink string `xorm:"-"` - StartTime timeutil.TimeStamp - EndTime timeutil.TimeStamp +func (task *Cloudbrain) ToShow() *CloudbrainShow { + return &CloudbrainShow{ + ID: task.ID, + JobType: task.JobType, + DisplayJobName: task.DisplayJobName, + Duration: task.TrainJobDuration, + ResourceSpec: GetCloudbrainResourceSpec(task.JobType, task.Type, task.ResourceSpecId, task.FlavorCode), + ComputeResource: task.ComputeResource, + } } func (task *Cloudbrain) ComputeAndSetDuration() { @@ -1917,11 +1871,11 @@ func GetStartedCloudbrainTaskByUpdatedUnix(startTime, endTime time.Time) ([]Clou return r, nil } -func GetCloudbrainByIds(ids []int64) ([]Cloudbrain, error) { +func GetCloudbrainByIds(ids []int64) ([]*Cloudbrain, error) { if len(ids) == 0 { return nil, nil } - cloudbrains := make([]Cloudbrain, 0) + cloudbrains := make([]*Cloudbrain, 0) err := x.In("id", ids).Unscoped().Find(&cloudbrains) if err != nil { return nil, err @@ -1930,31 +1884,128 @@ func GetCloudbrainByIds(ids []int64) ([]Cloudbrain, error) { } var ( - DebugResourceSpecs *ResourceSpecs - TrainResourceSpecs *ResourceSpecs + SpecsMapInitFlag = false + CloudbrainDebugResourceSpecsMap map[int]*ResourceSpec + CloudbrainTrainResourceSpecsMap map[int]*ResourceSpec + CloudbrainBenchmarkResourceSpecsMap map[int]*ResourceSpec + ModelArtsDebugResourceSpecsMap map[string]*FlavorInfo + ModelArtsTrainResourceSpecsMap map[string]*FlavorInfo ) -func GetResourceSpec(jobType string, resourceSpecId int) *ResourceSpec { - if jobType == string(JobTypeTrain) { - if TrainResourceSpecs == nil { - json.Unmarshal([]byte(setting.TrainResourceSpecs), &TrainResourceSpecs) +type ModelArtsFlavor struct { + Info []struct { + Code string `json:"code"` + Value string `json:"value"` + UnitPrice int64 `json:"unitPrice"` + } `json:"flavor"` +} + +func InitResourceSpecMap() { + if CloudbrainDebugResourceSpecsMap == nil || len(CloudbrainDebugResourceSpecsMap) == 0 { + t := ResourceSpecs{} + json.Unmarshal([]byte(setting.ResourceSpecs), &t) + CloudbrainDebugResourceSpecsMap = make(map[int]*ResourceSpec, len(t.ResourceSpec)) + for _, spec := range t.ResourceSpec { + CloudbrainDebugResourceSpecsMap[spec.Id] = spec } - for _, spec := range TrainResourceSpecs.ResourceSpec { - if resourceSpecId == spec.Id { - return spec - } + } + if CloudbrainTrainResourceSpecsMap == nil || len(CloudbrainTrainResourceSpecsMap) == 0 { + t := ResourceSpecs{} + json.Unmarshal([]byte(setting.TrainResourceSpecs), &t) + CloudbrainTrainResourceSpecsMap = make(map[int]*ResourceSpec, len(t.ResourceSpec)) + for _, spec := range t.ResourceSpec { + CloudbrainTrainResourceSpecsMap[spec.Id] = spec } - } else { - if DebugResourceSpecs == nil { - json.Unmarshal([]byte(setting.ResourceSpecs), &DebugResourceSpecs) + } + if CloudbrainBenchmarkResourceSpecsMap == nil || len(CloudbrainBenchmarkResourceSpecsMap) == 0 { + t := ResourceSpecs{} + json.Unmarshal([]byte(setting.BenchmarkResourceSpecs), &t) + CloudbrainBenchmarkResourceSpecsMap = make(map[int]*ResourceSpec, len(t.ResourceSpec)) + for _, spec := range t.ResourceSpec { + CloudbrainBenchmarkResourceSpecsMap[spec.Id] = spec + } + } + if ModelArtsDebugResourceSpecsMap == nil || len(ModelArtsDebugResourceSpecsMap) == 0 { + t := FlavorInfos{} + json.Unmarshal([]byte(setting.FlavorInfos), &t) + ModelArtsDebugResourceSpecsMap = make(map[string]*FlavorInfo, len(t.FlavorInfo)) + for _, spec := range t.FlavorInfo { + ModelArtsDebugResourceSpecsMap[spec.Value] = spec } - for _, spec := range DebugResourceSpecs.ResourceSpec { - if resourceSpecId == spec.Id { - return spec + } + if ModelArtsTrainResourceSpecsMap == nil || len(ModelArtsTrainResourceSpecsMap) == 0 { + t := ModelArtsFlavor{} + json.Unmarshal([]byte(setting.TrainJobFLAVORINFOS), &t) + ModelArtsTrainResourceSpecsMap = make(map[string]*FlavorInfo, len(t.Info)) + for _, spec := range t.Info { + f := &FlavorInfo{ + Value: spec.Code, + Desc: spec.Value, + UnitPrice: spec.UnitPrice, } + ModelArtsTrainResourceSpecsMap[spec.Value] = f } + } + SpecsMapInitFlag = true +} +type ResourceAndFlavor struct { + ResourceSpec *ResourceSpec + FlavorInfo *FlavorInfo +} + +func NewResourceAndFlavor(resourceSpec *ResourceSpec, flavorInfo *FlavorInfo) *ResourceAndFlavor { + return &ResourceAndFlavor{ + ResourceSpec: resourceSpec, + FlavorInfo: flavorInfo, + } +} + +func GetCloudbrainResourceSpec(jobType string, clusterType int, resourceSpecId int, flavorCode string) *ResourceAndFlavor { + if !SpecsMapInitFlag { + InitResourceSpecMap() } + if clusterType == TypeCloudBrainOne { + switch jobType { + case string(JobTypeDebug): + return NewResourceAndFlavor(CloudbrainDebugResourceSpecsMap[resourceSpecId], nil) + case string(JobTypeTrain): + return NewResourceAndFlavor(CloudbrainTrainResourceSpecsMap[resourceSpecId], nil) + case string(JobTypeBenchmark): + return NewResourceAndFlavor(CloudbrainBenchmarkResourceSpecsMap[resourceSpecId], nil) + + } + } else if clusterType == TypeCloudBrainTwo { + switch jobType { + case string(JobTypeDebug): + return NewResourceAndFlavor(nil, ModelArtsDebugResourceSpecsMap[flavorCode]) + case string(JobTypeTrain): + return NewResourceAndFlavor(nil, ModelArtsTrainResourceSpecsMap[flavorCode]) + case string(JobTypeInference): + return NewResourceAndFlavor(nil, ModelArtsTrainResourceSpecsMap[flavorCode]) + + } + } + return nil } + +func GetCloudbrainTaskUnitPrice(task Cloudbrain) int64 { + spec := GetCloudbrainResourceSpec(task.JobType, task.Type, task.ResourceSpecId, task.FlavorCode) + if spec == nil { + return 0 + } + if task.Type == TypeCloudBrainOne { + if spec.ResourceSpec == nil { + return 0 + } + return spec.ResourceSpec.UnitPrice + } else if task.Type == TypeCloudBrainTwo { + if spec.FlavorInfo == nil { + return 0 + } + return spec.FlavorInfo.UnitPrice + } + return 0 +} diff --git a/models/helper.go b/models/helper.go index a284424bb..55d4cac31 100644 --- a/models/helper.go +++ b/models/helper.go @@ -27,3 +27,11 @@ func valuesUser(m map[int64]*User) []*User { } return values } + +func valuesComment(m map[int64]*Comment) []*Comment { + var values = make([]*Comment, 0, len(m)) + for _, v := range m { + values = append(values, v) + } + return values +} diff --git a/models/reward_admin_log.go b/models/reward_admin_log.go index 24e3b8c47..fd79c3ed9 100644 --- a/models/reward_admin_log.go +++ b/models/reward_admin_log.go @@ -50,18 +50,18 @@ func UpdateRewardAdminLogStatus(logId string, oldStatus, newStatus int) error { return nil } -func GetRewardAdminLogByLogIds(logIds []string) ([]RewardAdminLog, error) { +func GetRewardAdminLogByLogIds(logIds []string) ([]*RewardAdminLog, error) { if len(logIds) == 0 { return nil, nil } - adminLogs := make([]AdminLogAndUser, 0) + adminLogs := make([]*AdminLogAndUser, 0) err := x.Table("reward_admin_log").Join("LEFT", "user", "reward_admin_log.creator_id = public.user.id").In("reward_admin_log.log_id", logIds).Find(&adminLogs) if err != nil { return nil, err } - r := make([]RewardAdminLog, len(adminLogs)) + r := make([]*RewardAdminLog, len(adminLogs)) for i, v := range adminLogs { - temp := v.AdminRewardAdminLog + temp := &v.AdminRewardAdminLog temp.CreatorName = v.User.Name r[i] = temp } diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 444e477b6..6e4b15e9d 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -119,7 +119,6 @@ type RewardRecordShowList []*RewardOperateRecordShow func (l *RewardRecordList) ToShow() (RewardRecordShowList, error) { actionMap, err := l.GetRewardRecordAction() - adminLogMap, err := l.GetRewardRecordAdminLog() CloudbrainMap, err := l.GetRewardRecordCloudbrainTask() if err != nil { return nil, err @@ -129,11 +128,9 @@ func (l *RewardRecordList) ToShow() (RewardRecordShowList, error) { temp := v.ToShow() switch v.SourceType { case SourceTypeAccomplishTask.Name(): - temp.Action = actionMap[v.SourceId] - case SourceTypeAdminOperate.Name(): - temp.AdminLog = adminLogMap[v.SourceId] + temp.Action = actionMap[v.SourceId].ToShow() case SourceTypeRunCloudbrainTask.Name(): - temp.Cloudbrain = CloudbrainMap[v.SourceId] + temp.Cloudbrain = CloudbrainMap[v.SourceId].ToShow() } result = append(result, &temp) } @@ -141,7 +138,7 @@ func (l *RewardRecordList) ToShow() (RewardRecordShowList, error) { return result, nil } -func (l *RewardRecordList) GetRewardRecordAction() (map[string]Action, error) { +func (l *RewardRecordList) GetRewardRecordAction() (map[string]*Action, error) { if len(*l) == 0 { return nil, nil } @@ -157,15 +154,15 @@ func (l *RewardRecordList) GetRewardRecordAction() (map[string]Action, error) { if err != nil { return nil, err } - result := make(map[string]Action, 0) + result := make(map[string]*Action, 0) for _, v := range actions { - result[fmt.Sprint(v.ID)] = *v + result[fmt.Sprint(v.ID)] = v } return result, nil } -func (l *RewardRecordList) GetRewardRecordAdminLog() (map[string]RewardAdminLog, error) { +func (l *RewardRecordList) GetRewardRecordAdminLog() (map[string]*RewardAdminLog, error) { if len(*l) == 0 { return nil, nil } @@ -180,7 +177,7 @@ func (l *RewardRecordList) GetRewardRecordAdminLog() (map[string]RewardAdminLog, if err != nil { return nil, err } - result := make(map[string]RewardAdminLog, 0) + result := make(map[string]*RewardAdminLog, 0) for _, v := range logs { result[fmt.Sprint(v.LogId)] = v } @@ -188,7 +185,7 @@ func (l *RewardRecordList) GetRewardRecordAdminLog() (map[string]RewardAdminLog, } -func (l *RewardRecordList) GetRewardRecordCloudbrainTask() (map[string]Cloudbrain, error) { +func (l *RewardRecordList) GetRewardRecordCloudbrainTask() (map[string]*Cloudbrain, error) { if len(*l) == 0 { return nil, nil } @@ -204,7 +201,7 @@ func (l *RewardRecordList) GetRewardRecordCloudbrainTask() (map[string]Cloudbrai if err != nil { return nil, err } - result := make(map[string]Cloudbrain, 0) + result := make(map[string]*Cloudbrain, 0) for _, v := range cloudbrains { result[fmt.Sprint(v.ID)] = v } @@ -242,7 +239,6 @@ type AdminRewardOperateReq struct { func (r RewardOperateRecord) ToShow() RewardOperateRecordShow { return RewardOperateRecordShow{ SerialNo: r.SerialNo, - CreateDate: r.CreatedUnix, OperateType: r.OperateType, Amount: r.Amount, Remark: r.Remark, @@ -255,7 +251,6 @@ func (r RewardOperateRecord) ToShow() RewardOperateRecordShow { type RewardOperateRecordShow struct { SerialNo string - CreateDate timeutil.TimeStamp Status string OperateType string Amount int64 @@ -263,9 +258,9 @@ type RewardOperateRecordShow struct { Remark string SourceType string LastOperateDate timeutil.TimeStamp - Action Action - Cloudbrain Cloudbrain - AdminLog RewardAdminLog + Action *ActionShow + Cloudbrain *CloudbrainShow + AdminLog *RewardAdminLog } func getPointOperateRecord(tl *RewardOperateRecord) (*RewardOperateRecord, error) { diff --git a/modules/auth/modelarts.go b/modules/auth/modelarts.go index 0cbed45a6..ce41f5d1e 100755 --- a/modules/auth/modelarts.go +++ b/modules/auth/modelarts.go @@ -22,7 +22,6 @@ type CreateModelArtsNotebookForm struct { Description string `form:"description"` Flavor string `form:"flavor" binding:"Required"` ImageId string `form:"image_id" binding:"Required"` - ResourceSpecId int `form:"resource_spec_id"` } func (f *CreateModelArtsNotebookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { @@ -47,7 +46,6 @@ type CreateModelArtsTrainJobForm struct { VersionName string `form:"version_name" binding:"Required"` FlavorName string `form:"flaver_names" binding:"Required"` EngineName string `form:"engine_names" binding:"Required"` - ResourceSpecId int `form:"resource_spec_id"` } type CreateModelArtsInferenceJobForm struct { @@ -73,7 +71,6 @@ type CreateModelArtsInferenceJobForm struct { ModelName string `form:"model_name" binding:"Required"` ModelVersion string `form:"model_version" binding:"Required"` CkptName string `form:"ckpt_name" binding:"Required"` - ResourceSpecId int `form:"resource_spec_id"` } func (f *CreateModelArtsTrainJobForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { diff --git a/modules/modelarts/modelarts.go b/modules/modelarts/modelarts.go index de5c392cd..9cc8c46c6 100755 --- a/modules/modelarts/modelarts.go +++ b/modules/modelarts/modelarts.go @@ -140,8 +140,9 @@ type VersionInfo struct { type Flavor struct { Info []struct { - Code string `json:"code"` - Value string `json:"value"` + Code string `json:"code"` + Value string `json:"value"` + UnitPrice int64 `json:"unitPrice"` } `json:"flavor"` } diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 29c8b97bb..a075f3b70 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -230,7 +230,7 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { command = commandTrain } - if !account.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, jobType, models.TypeCloudBrainOne, resourceSpecId, "") { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tpl, &form) @@ -318,7 +318,7 @@ func CloudBrainRestart(ctx *context.Context) { var status = string(models.JobWaiting) task := ctx.Cloudbrain for { - if !account.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, task.JobType, models.TypeCloudBrainOne, task.ResourceSpecId, "") { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) resultCode = "-1" errorMsg = models.ErrInsufficientPointsBalance{}.Error() @@ -1870,7 +1870,7 @@ func BenchMarkAlgorithmCreate(ctx *context.Context, form auth.CreateCloudBrainFo repo := ctx.Repo.Repository - if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeBenchmark), models.TypeCloudBrainOne, cloudbrain.BenchMarkResourceID, "") { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplCloudBrainBenchmarkNew, &form) @@ -2032,7 +2032,7 @@ func ModelBenchmarkCreate(ctx *context.Context, form auth.CreateCloudBrainForm) tpl := tplCloudBrainBenchmarkNew command := cloudbrain.Command - if !account.IsPointBalanceEnough(ctx.User.ID, jobType, resourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, jobType, models.TypeCloudBrainOne, resourceSpecId, "") { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, jobType, resourceSpecId) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tpl, &form) diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index bff9ec525..0b33a6dd7 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -205,10 +205,9 @@ func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm flavor := form.Flavor imageId := form.ImageId repo := ctx.Repo.Repository - resourceSpecId := form.ResourceSpecId - if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeDebug), resourceSpecId) { - log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeDebug), models.TypeCloudBrainTwo, 0, flavor) { + log.Error("point balance is not enough,userId=%d jobType=%s ", ctx.User.ID, string(models.JobTypeBenchmark)) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsNotebookNew, &form) return @@ -426,7 +425,7 @@ func NotebookManage(ctx *context.Context) { errorMsg = "you have no right to restart the job" break } - if !account.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.ResourceSpecId) { + if !account.IsPointBalanceEnough(ctx.User.ID, task.JobType, task.Type, task.ResourceSpecId, task.FlavorCode) { log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, task.JobType, task.ResourceSpecId) resultCode = "-1" errorMsg = models.ErrInsufficientPointsBalance{}.Error() @@ -1000,10 +999,9 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) FlavorName := form.FlavorName VersionCount := modelarts.VersionCount EngineName := form.EngineName - resourceSpecId := form.ResourceSpecId - if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeTrain), resourceSpecId) { - log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeTrain), models.TypeCloudBrainTwo, 0, flavorCode) { + log.Error("point balance is not enough,userId=%d jobType=%s", ctx.User.ID, string(models.JobTypeBenchmark)) cloudBrainNewDataPrepare(ctx) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsTrainJobNew, &form) return @@ -1183,7 +1181,6 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) EngineName: EngineName, VersionCount: VersionCount, TotalVersionCount: modelarts.TotalVersionCount, - ResourceSpecId: resourceSpecId, } //将params转换Parameters.Parameter,出错时返回给前端 @@ -1847,12 +1844,11 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference modelName := form.ModelName modelVersion := form.ModelVersion ckptName := form.CkptName - resourceSpecId := form.ResourceSpecId ckptUrl := form.TrainUrl + form.CkptName - if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeInference), resourceSpecId) { - log.Error("point balance is not enough,userId=%d jobType=%s resourceSpecId=%d", ctx.User.ID, string(models.JobTypeBenchmark), resourceSpecId) + if !account.IsPointBalanceEnough(ctx.User.ID, string(models.JobTypeInference), models.TypeCloudBrainTwo, 0, flavorCode) { + log.Error("point balance is not enough,userId=%d jobType=%s ", ctx.User.ID, string(models.JobTypeBenchmark)) inferenceJobErrorNewDataPrepare(ctx, form) ctx.RenderWithErr(models.ErrInsufficientPointsBalance{}.Error(), tplModelArtsInferenceJobNew, &form) return @@ -2002,7 +1998,6 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference ModelVersion: modelVersion, CkptName: ckptName, ResultUrl: resultObsPath, - ResourceSpecId: resourceSpecId, } err = modelarts.GenerateInferenceJob(ctx, req) diff --git a/services/reward/cloubrain_deduct.go b/services/reward/cloubrain_deduct.go index 7d0c39028..fdec1c0c1 100644 --- a/services/reward/cloubrain_deduct.go +++ b/services/reward/cloubrain_deduct.go @@ -20,9 +20,9 @@ func StartAndGetCloudBrainPointDeductTask(task models.Cloudbrain) (*models.Rewar return nil, nil } - spec := models.GetResourceSpec(task.JobType, task.ResourceSpecId) - if spec == nil || spec.UnitPrice == 0 { - log.Debug("GetResourceSpec failed,spec is nil or UnitPrice = 0") + unitPrice := models.GetCloudbrainTaskUnitPrice(task) + if unitPrice == 0 { + log.Debug("finish StartAndGetCloudBrainPointDeductTask, UnitPrice = 0 task.ID=%d", task.ID) return nil, nil } @@ -34,7 +34,7 @@ func StartAndGetCloudBrainPointDeductTask(task models.Cloudbrain) (*models.Rewar OperateType: models.OperateTypeDecrease, Delay: setting.CloudBrainPayDelay, Interval: setting.CloudBrainPayInterval, - UnitAmount: spec.UnitPrice, + UnitAmount: unitPrice, RewardType: models.RewardTypePoint, StartTime: time.Unix(int64(task.StartTime), 0), Tittle: RUN_CLOUDBRAIN_TASK_TITTLE, diff --git a/services/reward/point/account/point_account.go b/services/reward/point/account/point_account.go index c1d5722c4..79e98f2b2 100644 --- a/services/reward/point/account/point_account.go +++ b/services/reward/point/account/point_account.go @@ -67,12 +67,18 @@ func InitAccount(userId int64) (*models.PointAccount, error) { } //IsPointBalanceEnough check whether the user's point balance is bigger than task unit price -func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int) bool { +func IsPointBalanceEnough(targetUserId int64, jobType string, clusterType int, resourceSpecId int, flavorCode string) bool { if !setting.CloudBrainPaySwitch { return true } - spec := models.GetResourceSpec(jobType, resourceSpecId) - if spec == nil { + t := models.Cloudbrain{ + Type: clusterType, + JobType: jobType, + ResourceSpecId: resourceSpecId, + FlavorCode: flavorCode, + } + uniPrice := models.GetCloudbrainTaskUnitPrice(t) + if uniPrice == 0 { return true } a, err := GetAccount(targetUserId) @@ -80,6 +86,6 @@ func IsPointBalanceEnough(targetUserId int64, jobType string, resourceSpecId int log.Error("IsPointBalanceEnough GetAccount error,err=%v", err) return false } - return a.Balance >= spec.UnitPrice + return a.Balance >= uniPrice } diff --git a/services/task/task.go b/services/task/task.go index dcf7007c6..b53adb1f9 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -9,19 +9,13 @@ import ( ) func Accomplish(action models.Action) { + action.OpType = models.GetTaskOptType(action) switch action.OpType { case models.ActionCreateRepo, models.ActionCreateImage: if action.Repo.IsPrivate { return } - case models.ActionCreateDebugGPUTask, - models.ActionCreateDebugNPUTask, - models.ActionCreateTrainTask, - models.ActionCreateInferenceTask, - models.ActionCreateBenchMarkTask, - models.ActionCreateGPUTrainTask: - action.OpType = models.ActionCreateCloudbrainTask } go accomplish(action) } From 4332f2014ba7c62285a933c67e869fcbac7fd61c Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 30 Jun 2022 16:20:44 +0800 Subject: [PATCH 029/283] #2225 update --- models/cloudbrain.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/cloudbrain.go b/models/cloudbrain.go index 2f82640b7..ed52ad262 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -170,6 +170,7 @@ type Cloudbrain struct { type CloudbrainShow struct { ID int64 + Type int JobType string DisplayJobName string Duration string @@ -181,6 +182,7 @@ func (task *Cloudbrain) ToShow() *CloudbrainShow { return &CloudbrainShow{ ID: task.ID, JobType: task.JobType, + Type: task.Type, DisplayJobName: task.DisplayJobName, Duration: task.TrainJobDuration, ResourceSpec: GetCloudbrainResourceSpec(task.JobType, task.Type, task.ResourceSpecId, task.FlavorCode), From 0dbd239acce457cdea3c5f1e879a0aa738e94f14 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 30 Jun 2022 17:18:06 +0800 Subject: [PATCH 030/283] #2225 update --- models/cloudbrain.go | 2 ++ models/reward_operate_record.go | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/models/cloudbrain.go b/models/cloudbrain.go index ed52ad262..75ab1dfd6 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -170,6 +170,7 @@ type Cloudbrain struct { type CloudbrainShow struct { ID int64 + RepoFullName string Type int JobType string DisplayJobName string @@ -181,6 +182,7 @@ type CloudbrainShow struct { func (task *Cloudbrain) ToShow() *CloudbrainShow { return &CloudbrainShow{ ID: task.ID, + RepoFullName: task.Repo.FullName(), JobType: task.JobType, Type: task.Type, DisplayJobName: task.DisplayJobName, diff --git a/models/reward_operate_record.go b/models/reward_operate_record.go index 6e4b15e9d..aea39a875 100644 --- a/models/reward_operate_record.go +++ b/models/reward_operate_record.go @@ -201,9 +201,17 @@ func (l *RewardRecordList) GetRewardRecordCloudbrainTask() (map[string]*Cloudbra if err != nil { return nil, err } + var ids []int64 + for _, task := range cloudbrains { + ids = append(ids, task.RepoID) + } + repositoryMap, err := GetRepositoriesMapByIDs(ids) result := make(map[string]*Cloudbrain, 0) - for _, v := range cloudbrains { - result[fmt.Sprint(v.ID)] = v + if err == nil { + for _, v := range cloudbrains { + v.Repo = repositoryMap[v.RepoID] + result[fmt.Sprint(v.ID)] = v + } } return result, nil From 260ab0963342de1a64ed66b5e727e4c4c8d5a103 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Thu, 30 Jun 2022 18:03:21 +0800 Subject: [PATCH 031/283] #2225 fix bug --- services/reward/{cloubrain_deduct.go => cloudbrain_deduct.go} | 0 services/reward/period_task.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename services/reward/{cloubrain_deduct.go => cloudbrain_deduct.go} (100%) diff --git a/services/reward/cloubrain_deduct.go b/services/reward/cloudbrain_deduct.go similarity index 100% rename from services/reward/cloubrain_deduct.go rename to services/reward/cloudbrain_deduct.go diff --git a/services/reward/period_task.go b/services/reward/period_task.go index b6315b19f..c4180db56 100644 --- a/services/reward/period_task.go +++ b/services/reward/period_task.go @@ -77,7 +77,7 @@ func RunRewardTask(t models.RewardPeriodicTask, now time.Time) error { log.Error("RunRewardTask. operator of reward type is not exist") return errors.New("operator of reward type is not exist") } - nextTime := t.NextExecuteTime + nextTime := timeutil.TimeStamp(int64(t.NextExecuteTime) + t.IntervalSeconds) for i := 1; int64(i) <= n; i++ { err = operator.Operate(&models.RewardOperateContext{ SourceType: models.SourceTypeRunCloudbrainTask, From fe4f394ab73fd365e77695731c8c62cccfc1a8cc Mon Sep 17 00:00:00 2001 From: chenshihai Date: Thu, 30 Jun 2022 19:51:20 +0800 Subject: [PATCH 032/283] =?UTF-8?q?=E7=AE=97=E5=8A=9B=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/base/head_navbar.tmpl | 6 +- templates/base/head_navbar_fluid.tmpl | 2 + templates/base/head_navbar_home.tmpl | 6 +- templates/base/head_navbar_pro.tmpl | 6 +- templates/reward/point.tmpl | 6 + web_src/js/features/notification.js | 61 +++- web_src/vuepages/apis/modules/point.js | 41 +++ web_src/vuepages/apis/service.js | 26 ++ web_src/vuepages/pages/reward/point/const.js | 6 + .../vuepages/pages/reward/point/vp-point.js | 12 + .../vuepages/pages/reward/point/vp-point.vue | 293 ++++++++++++++++++ webpack.config.js | 6 + 12 files changed, 464 insertions(+), 7 deletions(-) create mode 100644 templates/reward/point.tmpl create mode 100644 web_src/vuepages/apis/modules/point.js create mode 100644 web_src/vuepages/apis/service.js create mode 100644 web_src/vuepages/pages/reward/point/const.js create mode 100644 web_src/vuepages/pages/reward/point/vp-point.js create mode 100644 web_src/vuepages/pages/reward/point/vp-point.vue diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 51761a7e5..0cf2f7484 100755 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -27,7 +27,8 @@ {{.i18n.Tr "issues"}} {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} - {{.i18n.Tr "repo.cloudbrain.task"}} + {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 @@ -58,7 +59,8 @@ {{.i18n.Tr "issues"}} {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} - {{.i18n.Tr "repo.cloudbrain.task"}} + {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 diff --git a/templates/base/head_navbar_fluid.tmpl b/templates/base/head_navbar_fluid.tmpl index 6baeced54..74827e12c 100644 --- a/templates/base/head_navbar_fluid.tmpl +++ b/templates/base/head_navbar_fluid.tmpl @@ -28,6 +28,7 @@ {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 @@ -58,6 +59,7 @@ {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 diff --git a/templates/base/head_navbar_home.tmpl b/templates/base/head_navbar_home.tmpl index c9ea13b8a..1864bee6e 100644 --- a/templates/base/head_navbar_home.tmpl +++ b/templates/base/head_navbar_home.tmpl @@ -19,7 +19,8 @@ {{.i18n.Tr "issues"}} {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} - {{.i18n.Tr "repo.cloudbrain.task"}} + {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 @@ -49,7 +50,8 @@ {{.i18n.Tr "issues"}} {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} - {{.i18n.Tr "repo.cloudbrain.task"}} + {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 diff --git a/templates/base/head_navbar_pro.tmpl b/templates/base/head_navbar_pro.tmpl index e744508f0..ba50c88fe 100644 --- a/templates/base/head_navbar_pro.tmpl +++ b/templates/base/head_navbar_pro.tmpl @@ -28,7 +28,8 @@ {{.i18n.Tr "issues"}} {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} - {{.i18n.Tr "repo.cloudbrain.task"}} + {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 @@ -59,7 +60,8 @@ {{.i18n.Tr "issues"}} {{.i18n.Tr "pull_requests"}} {{.i18n.Tr "milestones"}} - {{.i18n.Tr "repo.cloudbrain.task"}} + {{.i18n.Tr "repo.cloudbrain.task"}} + 算力积分 diff --git a/templates/reward/point.tmpl b/templates/reward/point.tmpl new file mode 100644 index 000000000..359564b35 --- /dev/null +++ b/templates/reward/point.tmpl @@ -0,0 +1,6 @@ +{{template "base/head" .}} + +
+
+ +{{template "base/footer" .}} diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js index 6f362eee6..aa8ed844e 100644 --- a/web_src/js/features/notification.js +++ b/web_src/js/features/notification.js @@ -47,7 +47,66 @@ export function initNotificationCount() { }); source.addEventListener('reward-operation', async (e) => { try { - console.log(e.data); + const data = JSON.parse(e.data); + const notice = $(` + + `); + $('body').append(notice); + notice.fadeIn(); + setTimeout(() => { + notice.fadeOut(); + }, 3000); + setTimeout(() => { + notice.remove(); + }, 5000); } catch (error) { console.error(error); } diff --git a/web_src/vuepages/apis/modules/point.js b/web_src/vuepages/apis/modules/point.js new file mode 100644 index 000000000..60b1df601 --- /dev/null +++ b/web_src/vuepages/apis/modules/point.js @@ -0,0 +1,41 @@ +import service from '../service'; + +// 算力积分概要 +export const getPointAccount = () => { + return service({ + url: '/reward/point/account', + method: 'get', + params: {}, + }); +} + +// 算力积分获取、消耗明细 +// operate-INCREASE 表示获取明细 DECREASE表示消耗明细, page-当前页, pageSize-每页条数 +export const getPointList = (params) => { + return service({ + url: '/reward/point/record/list', + method: 'get', + params, + }); +} + +// 管理员充值、扣减用户积分 +// TargetUserId, OperateType-INCREASE,DECREASE, Amount, Remark, RewardType-POINT +export const setPointOperate = (data) => { + return service({ + url: '/operation/reward/point/operate', + method: 'post', + data, + params: {} + }); +} + +// 算力积分页面 +export const getPoint = () => { + return service({ + url: '/reward/point', + method: 'get', + params: {}, + data: {}, + }); +} diff --git a/web_src/vuepages/apis/service.js b/web_src/vuepages/apis/service.js new file mode 100644 index 000000000..292b9ef78 --- /dev/null +++ b/web_src/vuepages/apis/service.js @@ -0,0 +1,26 @@ +import axios from 'axios'; + +const service = axios.create({ + baseURL: '/', + timeout: 20000, +}); + +service.interceptors.request.use((config) => { + config.data && Object.assign(config.data, { + _csrf: window.config ? window.config.csrf : '', + }); + config.params && Object.assign(config.params, { + _csrf: window.config ? window.config.csrf : '', + }); + return config; +}, (error) => { + return Promise.reject(error); +}); + +service.interceptors.response.use((response) => { + return response; +}, (error) => { + return Promise.reject(error); +}); + +export default service; diff --git a/web_src/vuepages/pages/reward/point/const.js b/web_src/vuepages/pages/reward/point/const.js new file mode 100644 index 000000000..af0332cc7 --- /dev/null +++ b/web_src/vuepages/pages/reward/point/const.js @@ -0,0 +1,6 @@ +export const SOURCE_TYPE = [{ k: 'ACCOMPLISH_TASK', v: '积分任务' }, { k: 'ADMIN_OPERATE', v: '管理员操作' }, { k: 'RUN_CLOUDBRAIN_TASK', v: '运行云脑任务' }]; +export const CONSUME_STATUS = [{ k: 'OPERATING', v: '进行中' }, { k: 'SUCCEEDED', v: '已完成' }]; +export const POINT_ACTIONS = [ + { k: 1, v: '创建公开项目' }, { k: 6, v: '每日提出任务' }, { k: 7, v: '每日提出PR' }, { k: 10, v: '发表评论' }, { k: 24, v: '上传数据集文件' }, { k: 30, v: '导入新模型' }, { k: 32, v: '完成微信扫码验证' }, + { k: 33, v: '每日运行云脑任务' }, { k: 34, v: '数据集被平台推荐' }, { k: 35, v: '提交新公开镜像' }, { k: 36, v: '镜像被平台推荐' }, { k: 37, v: '首次更换头像' }, { k: 38, v: '每日commit' }, { k: 39, v: '每日首次Fork项目' }, +]; diff --git a/web_src/vuepages/pages/reward/point/vp-point.js b/web_src/vuepages/pages/reward/point/vp-point.js new file mode 100644 index 000000000..8039d3f2c --- /dev/null +++ b/web_src/vuepages/pages/reward/point/vp-point.js @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import ElementUI from 'element-ui'; +import 'element-ui/lib/theme-chalk/index.css'; + +Vue.use(ElementUI); +import App from './vp-point.vue'; +// import App from '../manage/vp-point-manage.vue'; + +new Vue({ + el: '#__vue-root', + render: (h) => h(App), +}); diff --git a/web_src/vuepages/pages/reward/point/vp-point.vue b/web_src/vuepages/pages/reward/point/vp-point.vue new file mode 100644 index 000000000..22bb1f47e --- /dev/null +++ b/web_src/vuepages/pages/reward/point/vp-point.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/webpack.config.js b/webpack.config.js index cd3635427..8b8800150 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,6 +20,11 @@ for (const path of glob('web_src/less/themes/*.less')) { themes[parse(path).name] = [path]; } +const vuePages = {}; +for (const path of glob('web_src/vuepages/**/vp-*.js')) { + vuePages[parse(path).name] = [path]; +} + const isProduction = process.env.NODE_ENV !== 'development'; module.exports = { @@ -37,6 +42,7 @@ module.exports = { ], icons: glob('node_modules/@primer/octicons/build/svg/**/*.svg'), ...themes, + ...vuePages, }, devtool: false, output: { From 42d13cef3cf0c792d51475d40e6632a6c0552549 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 1 Jul 2022 11:59:14 +0800 Subject: [PATCH 033/283] #2225 update wechat bind rule --- models/wechat_bind.go | 4 ++++ modules/auth/wechat/bind.go | 6 +++--- routers/reward/point/point.go | 2 +- services/task/task.go | 11 +++++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/models/wechat_bind.go b/models/wechat_bind.go index b100221f2..ea005e0a6 100644 --- a/models/wechat_bind.go +++ b/models/wechat_bind.go @@ -96,3 +96,7 @@ func UnbindWechatOpenId(userId int64, oldWechatOpenID string) error { sess.Insert(logParam) return sess.Commit() } + +func CountWechatBindLog(wechatOpenId string, action WechatBindAction) (int64, error) { + return x.Where("wechat_open_id = ? and action = ?", action, wechatOpenId).Count(&WechatBindLog{}) +} diff --git a/modules/auth/wechat/bind.go b/modules/auth/wechat/bind.go index 7b4bffc02..e166aceb4 100644 --- a/modules/auth/wechat/bind.go +++ b/modules/auth/wechat/bind.go @@ -38,7 +38,7 @@ func (err WechatBindError) Error() string { } func BindWechat(userId int64, wechatOpenId string) error { - if !IsWechatAccountAvailable(userId, wechatOpenId) { + if !IsWechatAccountUsed(userId, wechatOpenId) { log.Error("bind wechat failed, because user use wrong wechat account to bind,userId=%d wechatOpenId=%s", userId, wechatOpenId) return NewWechatBindError(BIND_REPLY_WECHAT_ACCOUNT_USED) } @@ -60,9 +60,9 @@ func IsUserAvailableForWechatBind(userId int64, wechatOpenId string) bool { return currentOpenId == "" || currentOpenId == wechatOpenId } -//IsWechatAccountAvailable if wechat account used by another account,return false +//IsWechatAccountUsed if wechat account used by another account,return false //if wechat account not used or used by the given user,return true -func IsWechatAccountAvailable(userId int64, wechatOpenId string) bool { +func IsWechatAccountUsed(userId int64, wechatOpenId string) bool { user := models.GetUserByWechatOpenId(wechatOpenId) if user != nil && user.WechatOpenId != "" && user.ID != userId { return false diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index d912c1539..7ef57c0f9 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -48,7 +48,7 @@ func GetPointRecordList(ctx *context.Context) { } r, err := reward.GetRewardRecordList(models.RewardRecordListOpts{ - ListOptions: models.ListOptions{PageSize: 20, Page: page}, + ListOptions: models.ListOptions{PageSize: 10, Page: page}, UserId: ctx.User.ID, OperateType: t, RewardType: models.RewardTypePoint, diff --git a/services/task/task.go b/services/task/task.go index b53adb1f9..78f188997 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -16,6 +16,17 @@ func Accomplish(action models.Action) { if action.Repo.IsPrivate { return } + case models.ActionBindWechat: + n, err := models.CountWechatBindLog(action.Content, models.WECHAT_BIND) + if err != nil { + log.Error("CountWechatBindLog error when accomplish task,err=%v", err) + return + } + //if wechatOpenId has been bound before,the action can not get reward + if n > 1 { + return + } + } go accomplish(action) } From a18c391ae94e659f5c5209f7821f28ae83e67268 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 1 Jul 2022 14:37:57 +0800 Subject: [PATCH 034/283] #2225 fix bug --- models/task_accomplish_log.go | 6 +++--- services/reward/limiter/limiter.go | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/models/task_accomplish_log.go b/models/task_accomplish_log.go index 75494bfa2..582411561 100644 --- a/models/task_accomplish_log.go +++ b/models/task_accomplish_log.go @@ -30,11 +30,11 @@ func getTaskAccomplishLog(tl *TaskAccomplishLog) (*TaskAccomplishLog, error) { return tl, nil } -func CountTaskAccomplishLogInTaskPeriod(configId int64, userId int64, period *PeriodResult) (int64, error) { +func CountTaskAccomplishLogInTaskPeriod(taskCode string, userId int64, period *PeriodResult) (int64, error) { if period == nil { - return x.Where("config_id = ? and user_id = ?", configId, userId).Count(&TaskAccomplishLog{}) + return x.Where("task_code = ? and user_id = ?", taskCode, 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{}) + return x.Where("task_code = ? and user_id = ? and created_unix >= ? and created_unix < ? ", taskCode, userId, period.StartTime.Unix(), period.EndTime.Unix()).Count(&TaskAccomplishLog{}) } } diff --git a/services/reward/limiter/limiter.go b/services/reward/limiter/limiter.go index a73779ac1..d357ceabd 100644 --- a/services/reward/limiter/limiter.go +++ b/services/reward/limiter/limiter.go @@ -142,6 +142,9 @@ func (l *limiterRunner) limit(r models.LimitConfig) error { } if p != nil { redis_client.Expire(redisKey, p.LeftTime) + } else { + //add default expire time if no period set + redis_client.Expire(redisKey, 24*time.Hour) } } if usedNum > r.LimitNum { @@ -183,7 +186,7 @@ func (l *limiterRunner) LoadLimiters() error { func (l *limiterRunner) countInPeriod(r models.LimitConfig, p *models.PeriodResult) (int64, error) { switch r.LimitType { case models.LimitTypeTask.Name(): - return models.CountTaskAccomplishLogInTaskPeriod(r.ID, l.userId, p) + return models.CountTaskAccomplishLogInTaskPeriod(r.LimitCode, l.userId, p) case models.LimitTypeRewardPoint.Name(): return models.SumRewardAmountInTaskPeriod(models.RewardTypePoint.Name(), r.LimitCode, l.userId, p) default: From 4d2f89ca8023063131a77ce39f04b5eb6763840a Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 1 Jul 2022 16:39:35 +0800 Subject: [PATCH 035/283] #2225 fix bug --- models/attachment.go | 2 +- models/repo_watch.go | 5 +++++ models/reward_periodic_task.go | 2 +- services/task/task.go | 13 +++++++++++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/models/attachment.go b/models/attachment.go index 3dc8eac49..e7051c632 100755 --- a/models/attachment.go +++ b/models/attachment.go @@ -667,7 +667,7 @@ func Attachments(opts *AttachmentsOptions) ([]*AttachmentInfo, int64, error) { func GetAllDatasetContributorByDatasetId(datasetId int64) ([]*User, error) { r := make([]*User, 0) - if err := x.Select("distinct(user.*)").Table("attachment").Join("LEFT", "user", "user.ID = attachment.uploader_id").Where("attachment.dataset_id = ?", datasetId).Find(&r); err != nil { + if err := x.Select("distinct(public.user.*)").Table("attachment").Join("LEFT", "user", "public.user.ID = attachment.uploader_id").Where("attachment.dataset_id = ?", datasetId).Find(&r); err != nil { return nil, err } return r, nil diff --git a/models/repo_watch.go b/models/repo_watch.go index 864aec254..485874301 100644 --- a/models/repo_watch.go +++ b/models/repo_watch.go @@ -204,6 +204,11 @@ func notifyWatchers(e Engine, actions ...*Action) error { // Send the act to task chan ActionChan4Task <- *act + // If it has nothing to do with repo, return directly + if act.Repo == nil && act.RepoID == 0 { + return nil + } + if repoChanged { act.loadRepo() repo = act.Repo diff --git a/models/reward_periodic_task.go b/models/reward_periodic_task.go index 910f4fe8e..5e5466e86 100644 --- a/models/reward_periodic_task.go +++ b/models/reward_periodic_task.go @@ -90,7 +90,7 @@ func GetPeriodicTaskBySourceIdAndType(sourceType SourceType, sourceId string, op r := RewardPeriodicTask{} _, err := x.SQL("select rpt.* from reward_periodic_task rpt "+ "inner join reward_operate_record ror on rpt.operate_serial_no = ror.serial_no"+ - " where ror.source_type = ? and source_id = ? and operate_type = ? ", sourceType.Name(), sourceId, operateType.Name()).Get(&r) + " where ror.source_type = ? and ror.source_id = ? and ror.operate_type = ? ", sourceType.Name(), sourceId, operateType.Name()).Get(&r) if err != nil { return nil, err } diff --git a/services/task/task.go b/services/task/task.go index 78f188997..5fc5e9bcb 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -9,13 +9,22 @@ import ( ) func Accomplish(action models.Action) { + defer func() { + if err := recover(); err != nil { + combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) + log.Error("PANIC:%v", combinedErr) + } + }() action.OpType = models.GetTaskOptType(action) switch action.OpType { - case models.ActionCreateRepo, - models.ActionCreateImage: + case models.ActionCreateRepo: if action.Repo.IsPrivate { return } + case models.ActionCreateImage: + if action.IsPrivate { + return + } case models.ActionBindWechat: n, err := models.CountWechatBindLog(action.Content, models.WECHAT_BIND) if err != nil { From 6730d4ca9370ad1d74da8f60d9480dc89d3fe4d2 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 1 Jul 2022 18:15:18 +0800 Subject: [PATCH 036/283] #2225 update --- models/action.go | 38 ++++++++++++----- models/action_list.go | 61 +++++++++++++++++++++++++-- models/cloudbrain.go | 8 ++++ models/helper.go | 7 +++ modules/notification/action/action.go | 12 ++---- modules/notification/base/notifier.go | 2 +- modules/notification/base/null.go | 2 +- modules/notification/notification.go | 4 +- routers/image/image.go | 5 ++- 9 files changed, 113 insertions(+), 26 deletions(-) diff --git a/models/action.go b/models/action.go index 33322a921..1a25c162a 100755 --- a/models/action.go +++ b/models/action.go @@ -89,6 +89,7 @@ type Action struct { IsTransformed bool `xorm:"INDEX NOT NULL DEFAULT false"` Content string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + Cloudbrain *Cloudbrain `xorm:"-"` } type ActionShow struct { @@ -100,6 +101,7 @@ type ActionShow struct { IssueInfos []string CommentLink string IssueTitle string + Cloudbrain *CloudbrainShow4Action } // GetOpType gets the ActionType of this action. @@ -256,22 +258,25 @@ func (a *Action) ToShow() *ActionShow { actionShow.CommentLink = a.GetCommentLink() } + if a.Cloudbrain != nil { + c := &CloudbrainShow4Action{ + ID: a.Cloudbrain.ID, + Type: a.Cloudbrain.Type, + JobType: a.Cloudbrain.JobType, + DisplayJobName: a.Cloudbrain.DisplayJobName, + ComputeResource: a.Cloudbrain.ComputeResource, + } + actionShow.Cloudbrain = c + } + return actionShow } func GetTaskOptType(action Action) ActionType { - switch action.OpType { - case ActionCreateDebugGPUTask, - ActionCreateDebugNPUTask, - ActionCreateTrainTask, - ActionCreateInferenceTask, - ActionCreateBenchMarkTask, - ActionCreateGPUTrainTask: + if action.IsCloudbrainAction() { return ActionCreateCloudbrainTask - default: - return action.OpType } - + return action.OpType } // GetRepositoryFromMatch returns a *Repository from a username and repo strings @@ -371,6 +376,19 @@ func (a *Action) GetIssueContent() string { return issue.Content } +func (a *Action) IsCloudbrainAction() bool { + switch a.OpType { + case ActionCreateDebugGPUTask, + ActionCreateDebugNPUTask, + ActionCreateTrainTask, + ActionCreateInferenceTask, + ActionCreateBenchMarkTask, + ActionCreateGPUTrainTask: + return true + } + return false +} + // GetFeedsOptions options for retrieving feeds type GetFeedsOptions struct { RequestedUser *User // the user we want activity for diff --git a/models/action_list.go b/models/action_list.go index a0987c20d..17700edbd 100644 --- a/models/action_list.go +++ b/models/action_list.go @@ -4,7 +4,10 @@ package models -import "fmt" +import ( + "fmt" + "strconv" +) // ActionList defines a list of actions type ActionList []*Action @@ -111,7 +114,9 @@ func (actions ActionList) loadComments(e Engine) ([]*Comment, error) { } for _, action := range actions { - action.Comment = commentMaps[action.CommentID] + if action.CommentID > 0 { + action.Comment = commentMaps[action.CommentID] + } } return valuesComment(commentMaps), nil } @@ -121,6 +126,52 @@ func (actions ActionList) LoadComments() ([]*Comment, error) { return actions.loadComments(x) } +func (actions ActionList) getCloudbrainIDs() []int64 { + cloudbrainIDs := make(map[int64]struct{}, 0) + for _, action := range actions { + if !action.IsCloudbrainAction() { + continue + } + cloudbrainId, _ := strconv.ParseInt(action.Content, 10, 64) + if _, ok := cloudbrainIDs[cloudbrainId]; !ok { + cloudbrainIDs[cloudbrainId] = struct{}{} + } + } + return keysInt64(cloudbrainIDs) +} + +func (actions ActionList) loadCloudbrains(e Engine) ([]*Cloudbrain, error) { + if len(actions) == 0 { + return nil, nil + } + + cloudbrainIDs := actions.getCloudbrainIDs() + + cloudbrainMaps := make(map[int64]*Cloudbrain, len(cloudbrainIDs)) + if len(cloudbrainIDs) == 0 { + return make([]*Cloudbrain, 0), nil + } + err := e. + In("id", cloudbrainIDs).Unscoped(). + Find(&cloudbrainMaps) + if err != nil { + return nil, fmt.Errorf("find cloudbrain: %v", err) + } + + for _, action := range actions { + cloudbrainId, _ := strconv.ParseInt(action.Content, 10, 64) + if cloudbrainId > 0 { + action.Cloudbrain = cloudbrainMaps[cloudbrainId] + } + } + return valuesCloudbrain(cloudbrainMaps), nil +} + +// LoadComments loads actions' all comments +func (actions ActionList) LoadCloudbrains() ([]*Comment, error) { + return actions.loadComments(x) +} + // loadAttributes loads all attributes func (actions ActionList) loadAttributes(e Engine) (err error) { if _, err = actions.loadUsers(e); err != nil { @@ -140,7 +191,7 @@ func (actions ActionList) LoadAttributes() error { } // LoadAllAttributes loads all attributes of the actions -// compare with LoadAttributes() ,LoadAllAttributes() loads Comment attribute +// compare with LoadAttributes() ,LoadAllAttributes() loads Comment and Cloudbrain attribute func (actions ActionList) LoadAllAttributes() error { return actions.loadAllAttributes(x) } @@ -159,5 +210,9 @@ func (actions ActionList) loadAllAttributes(e Engine) (err error) { return } + if _, err = actions.loadCloudbrains(e); err != nil { + return + } + return nil } diff --git a/models/cloudbrain.go b/models/cloudbrain.go index 75ab1dfd6..1d2e56476 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -179,6 +179,14 @@ type CloudbrainShow struct { ComputeResource string } +type CloudbrainShow4Action struct { + ID int64 + Type int + JobType string + DisplayJobName string + ComputeResource string +} + func (task *Cloudbrain) ToShow() *CloudbrainShow { return &CloudbrainShow{ ID: task.ID, diff --git a/models/helper.go b/models/helper.go index 55d4cac31..e381f1e37 100644 --- a/models/helper.go +++ b/models/helper.go @@ -35,3 +35,10 @@ func valuesComment(m map[int64]*Comment) []*Comment { } return values } +func valuesCloudbrain(m map[int64]*Cloudbrain) []*Cloudbrain { + var values = make([]*Cloudbrain, 0, len(m)) + for _, v := range m { + values = append(values, v) + } + return values +} diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index 6a43c6e9a..bfe574328 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -375,7 +375,7 @@ func (t *actionNotifier) NotifyDatasetRecommend(optUser *models.User, dataset *m ActUser: user, RepoID: dataset.RepoID, Repo: dataset.Repo, - Content: fmt.Sprint(dataset.ID), + Content: fmt.Sprintf("%d|%s", dataset.ID, dataset.Title), }) } if err := models.NotifyWatchers(actions...); err != nil { @@ -390,18 +390,14 @@ func (t *actionNotifier) NotifyCreateImage(doer *models.User, image models.Image ActUser: doer, OpType: models.ActionCreateImage, IsPrivate: image.IsPrivate, - Content: fmt.Sprint(image.ID), + Content: fmt.Sprintf("%d|%s", image.ID, image.Tag), } if err := models.NotifyWatchers(act); err != nil { log.Error("notifyWatchers: %v", err) } } -func (t *actionNotifier) NotifyImageRecommend(optUser *models.User, imageId int64, action string) { - image, err := models.GetImageByID(imageId) - if err != nil { - return - } +func (t *actionNotifier) NotifyImageRecommend(optUser *models.User, image *models.Image, action string) { u, err := models.GetUserByID(image.UID) if err != nil { return @@ -413,7 +409,7 @@ func (t *actionNotifier) NotifyImageRecommend(optUser *models.User, imageId int6 ActUser: u, OpType: models.ActionImageRecommend, IsPrivate: false, - Content: fmt.Sprint(imageId), + Content: fmt.Sprintf("%d|%s", image.ID, image.Tag), } if err := models.NotifyWatchers(act); err != nil { log.Error("notifyWatchers: %v", err) diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 7673a5909..1429dc090 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -60,6 +60,6 @@ type Notifier interface { NotifyWechatBind(user *models.User, wechatOpenId string) NotifyDatasetRecommend(optUser *models.User, dataset *models.Dataset, action string) NotifyCreateImage(doer *models.User, image models.Image) - NotifyImageRecommend(optUser *models.User, imageId int64, action string) + NotifyImageRecommend(optUser *models.User, image *models.Image, action string) NotifyChangeUserAvatar(user *models.User, form auth.AvatarForm) } diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index eea5c5e77..27ed24f15 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -170,7 +170,7 @@ func (*NullNotifier) NotifyDatasetRecommend(optUser *models.User, dataset *model func (*NullNotifier) NotifyCreateImage(doer *models.User, image models.Image) { } -func (*NullNotifier) NotifyImageRecommend(optUser *models.User, imageId int64, action string) { +func (*NullNotifier) NotifyImageRecommend(optUser *models.User, image *models.Image, action string) { } func (*NullNotifier) NotifyChangeUserAvatar(user *models.User, form auth.AvatarForm) { diff --git a/modules/notification/notification.go b/modules/notification/notification.go index d652dc043..6c96d58da 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -293,9 +293,9 @@ func NotifyCreateImage(doer *models.User, image models.Image) { } // NotifyDatasetRecommend -func NotifyImageRecommend(optUser *models.User, imageId int64, action string) { +func NotifyImageRecommend(optUser *models.User, image *models.Image, action string) { for _, notifier := range notifiers { - notifier.NotifyImageRecommend(optUser, imageId, action) + notifier.NotifyImageRecommend(optUser, image, action) } } diff --git a/routers/image/image.go b/routers/image/image.go index e238387ab..35b6b943b 100644 --- a/routers/image/image.go +++ b/routers/image/image.go @@ -26,7 +26,10 @@ func Action(ctx *context.Context) { if err != nil { ctx.JSON(http.StatusOK, models.BaseErrorMessage(ctx.Tr("repo.star_fail", ctx.Params(":action")))) } else { - notification.NotifyImageRecommend(ctx.User, imageId, ctx.Params(":action")) + image, err := models.GetImageByID(imageId) + if err == nil { + notification.NotifyImageRecommend(ctx.User, image, ctx.Params(":action")) + } ctx.JSON(http.StatusOK, models.BaseOKMessage) } } From d03ec1ba7cf96d8861ad4cfade7f3b7e9911fda7 Mon Sep 17 00:00:00 2001 From: chenshihai Date: Fri, 1 Jul 2022 18:47:11 +0800 Subject: [PATCH 037/283] =?UTF-8?q?=E7=AE=97=E5=8A=9B=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_src/vuepages/components/BaseDialog.vue | 113 ++++++++++++++++ web_src/vuepages/pages/reward/point/utils.js | 98 ++++++++++++++ .../vuepages/pages/reward/point/vp-point.vue | 126 ++++++++++-------- 3 files changed, 282 insertions(+), 55 deletions(-) create mode 100644 web_src/vuepages/components/BaseDialog.vue create mode 100644 web_src/vuepages/pages/reward/point/utils.js diff --git a/web_src/vuepages/components/BaseDialog.vue b/web_src/vuepages/components/BaseDialog.vue new file mode 100644 index 000000000..a95f99d18 --- /dev/null +++ b/web_src/vuepages/components/BaseDialog.vue @@ -0,0 +1,113 @@ + + + diff --git a/web_src/vuepages/pages/reward/point/utils.js b/web_src/vuepages/pages/reward/point/utils.js new file mode 100644 index 000000000..b19e41b1f --- /dev/null +++ b/web_src/vuepages/pages/reward/point/utils.js @@ -0,0 +1,98 @@ + +import { formatDate } from 'element-ui/lib/utils/date-util'; +import { SOURCE_TYPE, CONSUME_STATUS, POINT_ACTIONS } from './const'; + +const getSourceType = (key) => { + const find = SOURCE_TYPE.filter(item => item.k === key); + return find.length ? find[0].v : key; +}; +const getConsumeStatus = (key) => { + const find = CONSUME_STATUS.filter(item => item.k === key); + return find.length ? find[0].v : key; +}; +const getPointAction = (key) => { + const find = POINT_ACTIONS.filter(item => item.k === key); + return find.length ? find[0].v : key; +}; + +export const getRewardPointRecordInfo = (record) => { + const out = { + sn: record.SerialNo, + date: formatDate(new Date(record.LastOperateDate * 1000), 'yyyy-MM-DD HH:mm:ss'), + _status: record.Status, + status: getConsumeStatus(record.Status) || '--', + statusColor: record.Status === 'OPERATING' ? 'rgb(33, 186, 69)' : '', + _sourceType: record.SourceType, + sourceType: getSourceType(record.SourceType), + duration: record?.Cloudbrain?.Duration || '--', + taskName: record?.Cloudbrain?.DisplayJobName || '--', + taskId: record?.Cloudbrain?.ID, + action: record?.Action?.OpType ? getPointAction(record.Action.OpType) : '--', + remark: record.Remark, + amount: record.Amount, + }; + if (record.OperateType === 'INCREASE') { + if (record.SourceType === 'ADMIN_OPERATE') { + out.remark = record.Remark; + } else if (record.SourceType === 'ACCOMPLISH_TASK') { + switch (record?.Action?.OpType) { + case 1: // 创建公开项目 - 创建了项目OpenI/aiforge + out.remark = `创建了项目${record.Action.ShortRepoFullDisplayName}`; + break; + case 6: // 每日提出任务 - 创建了任务PCL-Platform.Intelligence/AISynergy#19 + out.remark = `创建了任务${record.Action.ShortRepoFullDisplayName}#${record.Action.IssueInfos[0]}`; + break; + case 7: // 每日提出PR - 创建了合并请求OpenI/aiforge#1 + out.remark = `创建了合并请求${record.Action.ShortRepoFullDisplayName}#${record.Action.IssueInfos[0]}`; + break; + case 10: // 发表评论 - 评论了任务PCL-Platform.Intelligence/AISynergy#19 + out.remark = `评论了任务${record.Action.ShortRepoFullDisplayName}#${record.Action.IssueInfos[0]}`; + break; + case 24: // 上传数据集文件 - 上传了数据集文件MMISTData.zip + out.remark = `上传了数据集文件${record.Action.RefName}`; + break; + case 30: // 导入新模型 - 导入了新模型resnet50_qx7l + break; + case 32: // 完成微信扫码验证 - 首次绑定微信奖励 + out.remark = '首次绑定微信奖励'; + break; + case 33: // 每日运行云脑任务 - 创建了(CPU/GPU/NPU)类型(调试/训练/推理/评测)任务tangl202204131431995 + out.remark = `创建了{{}}类型{{}}任务${record.Action.RefName}`; + break; + case 34: // 数据集被平台推荐 - 数据集XXX被设置为推荐数据集 + out.remark = `数据集${record.Action.RefName}被设置为推荐数据集`; + break; + case 35: // 提交新公开镜像 - 提交了镜像jiangxiang_ceshi_tang03 + out.remark = `提交了镜像${record.Action.RefName}`; + break; + case 36: // 镜像被平台推荐 - 镜像XXX被设置为推荐镜像 + out.remark = `镜像${record.Action.RefName}被设置为推荐镜像`; + break; + case 37: // 首次更换头像 - 更新了头像 + out.remark = '更新了头像'; + break; + case 38: // 每日commit - 推送了xxxx分支的代码到OpenI/aiforge + const words = record.Action.RefName.split('/'); + const branch = words[words.length - 1]; + out.remark = `推送了${branch}分支的代码到${record.Action.ShortRepoFullDisplayName}`; + break; + case 39: // 每日首次Fork项目 - 创建了项目OpenI/fork_aiforge + out.remark = `创建了项目${record.Action.ShortRepoFullDisplayName}`; + break; + default: + break; + } + } else if (record.SourceType === 'RUN_CLOUDBRAIN_TASK') { + + } + } else if (record.OperateType === 'DECREASE') { + if (record.SourceType === 'ADMIN_OPERATE') { + out.remark = record.Remark; + } else if (record.SourceType === 'ACCOMPLISH_TASK') { + + } else if (record.SourceType === 'RUN_CLOUDBRAIN_TASK') { + + } + } + return out; +}; diff --git a/web_src/vuepages/pages/reward/point/vp-point.vue b/web_src/vuepages/pages/reward/point/vp-point.vue index 22bb1f47e..65b48f1c9 100644 --- a/web_src/vuepages/pages/reward/point/vp-point.vue +++ b/web_src/vuepages/pages/reward/point/vp-point.vue @@ -41,30 +41,53 @@
-
- - - - +
+ + + + + + + + - - - + + + +
+
+ + - + - + + - + + + + - + + @@ -88,7 +111,7 @@ From 097c76d7f1f423209944a3379cffca2153895bc1 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Mon, 4 Jul 2022 17:41:25 +0800 Subject: [PATCH 038/283] #2225 update --- models/action.go | 25 ++++++++----------------- models/action_list.go | 6 ++++++ models/cloudbrain.go | 11 ++++++++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/models/action.go b/models/action.go index 3e6a58077..c3cf7e9f2 100755 --- a/models/action.go +++ b/models/action.go @@ -60,23 +60,14 @@ const ( ActionCreateGPUTrainTask //31 ActionCreateGrampusNPUTrainTask //32 ActionCreateGrampusGPUTrainTask //33 - ActionUploadAttachment //24 - ActionCreateDebugGPUTask //25 - ActionCreateDebugNPUTask //26 - ActionCreateTrainTask //27 - ActionCreateInferenceTask // 28 - ActionCreateBenchMarkTask //29 - ActionCreateNewModelTask //30 - ActionCreateGPUTrainTask //31 - - ActionBindWechat //32issue_assignees - ActionCreateCloudbrainTask //33 - ActionDatasetRecommended //34 - ActionCreateImage //35 - ActionImageRecommend //36 - ActionChangeUserAvatar //37 - ActionPushCommits //38 - ActionForkRepo //39 + ActionBindWechat //34 + ActionCreateCloudbrainTask //35 + ActionDatasetRecommended //36 + ActionCreateImage //37 + ActionImageRecommend //38 + ActionChangeUserAvatar //39 + ActionPushCommits //40 + ActionForkRepo //41 ) diff --git a/models/action_list.go b/models/action_list.go index 17700edbd..0a355d0ce 100644 --- a/models/action_list.go +++ b/models/action_list.go @@ -29,6 +29,9 @@ func (actions ActionList) loadUsers(e Engine) ([]*User, error) { userIDs := actions.getUserIDs() userMaps := make(map[int64]*User, len(userIDs)) + if len(userIDs) == 0 { + return make([]*User, 0), nil + } err := e. In("id", userIDs). Find(&userMaps) @@ -64,6 +67,9 @@ func (actions ActionList) loadRepositories(e Engine) ([]*Repository, error) { repoIDs := actions.getRepoIDs() repoMaps := make(map[int64]*Repository, len(repoIDs)) + if len(repoIDs) == 0 { + return make([]*Repository, 0), nil + } err := e. In("id", repoIDs). Find(&repoMaps) diff --git a/models/cloudbrain.go b/models/cloudbrain.go index d2d4ac656..0f5707e3c 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -188,6 +188,7 @@ type CloudbrainShow struct { Duration string ResourceSpec *ResourceAndFlavor ComputeResource string + AiCenter string } type CloudbrainShow4Action struct { @@ -199,9 +200,9 @@ type CloudbrainShow4Action struct { } func (task *Cloudbrain) ToShow() *CloudbrainShow { - return &CloudbrainShow{ - ID: task.ID, - RepoFullName: task.Repo.FullName(), + c := &CloudbrainShow{ + ID: task.ID, + JobType: task.JobType, Type: task.Type, DisplayJobName: task.DisplayJobName, @@ -209,6 +210,10 @@ func (task *Cloudbrain) ToShow() *CloudbrainShow { ResourceSpec: GetCloudbrainResourceSpec(task.JobType, task.Type, task.ResourceSpecId, task.FlavorCode), ComputeResource: task.ComputeResource, } + if task.Repo != nil { + c.RepoFullName = task.Repo.FullName() + } + return c } func (task *Cloudbrain) ComputeAndSetDuration() { From 3a0aa205af9d029c6285698ee7b4a34e638c75d1 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 5 Jul 2022 09:32:49 +0800 Subject: [PATCH 039/283] #2225 remove issueTittle from ActionShow --- models/action.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/models/action.go b/models/action.go index c3cf7e9f2..b5b3381e3 100755 --- a/models/action.go +++ b/models/action.go @@ -101,7 +101,6 @@ type ActionShow struct { RefName string IssueInfos []string CommentLink string - IssueTitle string Cloudbrain *CloudbrainShow4Action } @@ -246,9 +245,8 @@ func (a *Action) ToShow() *ActionShow { actionShow.Content = a.Content actionShow.RefName = a.RefName - if strings.Contains(a.Content, "|") { + if strings.Contains(a.Content, "|") && a.IsIssueAction() { actionShow.IssueInfos = a.GetIssueInfos() - actionShow.IssueTitle = a.GetIssueTitle() } if a.Repo != nil { @@ -390,6 +388,24 @@ func (a *Action) IsCloudbrainAction() bool { return false } +func (a *Action) IsIssueAction() bool { + switch a.OpType { + case ActionCreateIssue, + ActionCloseIssue, + ActionClosePullRequest, + ActionReopenIssue, + ActionReopenPullRequest, + ActionCommentPull, + ActionCommentIssue, + ActionCreatePullRequest, + ActionApprovePullRequest, + ActionRejectPullRequest, + ActionMergePullRequest: + return true + } + return false +} + // GetFeedsOptions options for retrieving feeds type GetFeedsOptions struct { RequestedUser *User // the user we want activity for From 66fb71107f553238a3dac202ef7ac5ffb9da80d8 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Tue, 5 Jul 2022 16:22:19 +0800 Subject: [PATCH 040/283] #2225 add point rule route --- routers/reward/point/point.go | 5 +++++ routers/routes/routes.go | 1 + 2 files changed, 6 insertions(+) diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index 7ef57c0f9..3828a2900 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -11,6 +11,7 @@ import ( ) const tplPoint base.TplName = "reward/point" +const tplPointRule base.TplName = "reward/point/rule" type AccountResponse struct { Balance int64 @@ -80,3 +81,7 @@ func OperatePointAccountBalance(ctx *context.Context, req models.AdminRewardOper func GetPointPage(ctx *context.Context) { ctx.HTML(200, tplPoint) } + +func GetRulePage(ctx *context.Context) { + ctx.HTML(200, tplPointRule) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 00a820fc9..52504c388 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -1355,6 +1355,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/reward/point", func() { m.Get("", point.GetPointPage) + m.Get("/rule", point.GetRulePage) m.Get("/account", point.GetPointAccount) m.Get("/record/list", point.GetPointRecordList) }, reqSignIn) From 77cdc92ef4f14917431171bedc9df1cbad4e160f Mon Sep 17 00:00:00 2001 From: chenshihai Date: Tue, 5 Jul 2022 17:15:10 +0800 Subject: [PATCH 041/283] =?UTF-8?q?=E7=AE=97=E5=8A=9B=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/reward/point/rule.tmpl | 117 ++++++++++++++++++ web_src/vuepages/pages/reward/point/const.js | 5 +- web_src/vuepages/pages/reward/point/utils.js | 80 +++++++++--- .../vuepages/pages/reward/point/vp-point.js | 1 - .../vuepages/pages/reward/point/vp-point.vue | 28 ++--- 5 files changed, 191 insertions(+), 40 deletions(-) create mode 100644 templates/reward/point/rule.tmpl diff --git a/templates/reward/point/rule.tmpl b/templates/reward/point/rule.tmpl new file mode 100644 index 000000000..645a2c96c --- /dev/null +++ b/templates/reward/point/rule.tmpl @@ -0,0 +1,117 @@ +{{template "base/head_home" .}} + +
+

个人算力积分奖励规则

+
+

说明:单日用户积分的获取上限为50分。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
奖励名称获取积分值上限值奖励细节澄清
完成微信扫码验证50累计积分获取上限501、首次完成微信扫码验证,即获取50积分。
2、同个账号,更换微信号码再验证不重复给积分。
3、同一个微信,绑定第一个账号时奖励50分,下次绑定其他账号时不再奖励。
每日首次Fork项目1每日积分获取上限1
创建公开项目1每日积分获取上限3请注意项目质量,请勿复制粘贴或者重复公开项目,任何非常规的以公开项目去获取积分的行为将被认定为积分舞弊,将扣除所有积分。
每日提出PR1每日积分获取上限3
每日commit1每日积分获取上限3通过前台界面和后台命令行方式commit,都可获得奖励积分。
每日提出任务1每日积分获取上限3
发表评论1每日积分获取上限2禁止空评论或评论后马上删除等非正常获取积分的方式,一经发现将扣除所有积分。
上传数据集文件1每日积分获取上限1请注意数据集质量,请勿复制粘贴或者重复公开数据集,任何非常规的以公开数据集去获取积分的行为将被认定为积分舞弊,将扣除所有积分。
数据集被平台推荐5每日积分获取上限15仅统计属于个人的数据集,属于组织的数据集暂不统计。
导入新模型1每日积分获取上限3请注意模型质量,请勿重复导入相同模型,任何非常规的以导入新模型去获取 积分的行为将被认定为积分舞弊,将扣除所有积分。
每日运行云脑任务10每日积分获取上限10 每日运行调试、训练、推理、评测中任何一种任务,即可获得。
提交新公开镜像1每日积分获取上限3
镜像被平台推荐5每日积分获取上限15
首次更换头像2累计积分获取上限2首次更换头像,积分+2。
+ +
+{{template "base/footer" .}} \ No newline at end of file diff --git a/web_src/vuepages/pages/reward/point/const.js b/web_src/vuepages/pages/reward/point/const.js index af0332cc7..a0fe162e5 100644 --- a/web_src/vuepages/pages/reward/point/const.js +++ b/web_src/vuepages/pages/reward/point/const.js @@ -1,6 +1,7 @@ export const SOURCE_TYPE = [{ k: 'ACCOMPLISH_TASK', v: '积分任务' }, { k: 'ADMIN_OPERATE', v: '管理员操作' }, { k: 'RUN_CLOUDBRAIN_TASK', v: '运行云脑任务' }]; export const CONSUME_STATUS = [{ k: 'OPERATING', v: '进行中' }, { k: 'SUCCEEDED', v: '已完成' }]; export const POINT_ACTIONS = [ - { k: 1, v: '创建公开项目' }, { k: 6, v: '每日提出任务' }, { k: 7, v: '每日提出PR' }, { k: 10, v: '发表评论' }, { k: 24, v: '上传数据集文件' }, { k: 30, v: '导入新模型' }, { k: 32, v: '完成微信扫码验证' }, - { k: 33, v: '每日运行云脑任务' }, { k: 34, v: '数据集被平台推荐' }, { k: 35, v: '提交新公开镜像' }, { k: 36, v: '镜像被平台推荐' }, { k: 37, v: '首次更换头像' }, { k: 38, v: '每日commit' }, { k: 39, v: '每日首次Fork项目' }, + { k: 1, v: '创建公开项目' }, { k: 6, v: '每日提出任务' }, { k: 7, v: '每日提出PR' }, { k: 10, v: '发表评论' }, { k: 24, v: '上传数据集文件' }, { k: 30, v: '导入新模型' }, { k: 34, v: '完成微信扫码验证' }, + { k: 35, v: '每日运行云脑任务' }, { k: 36, v: '数据集被平台推荐' }, { k: 37, v: '提交新公开镜像' }, { k: 38, v: '镜像被平台推荐' }, { k: 39, v: '首次更换头像' }, { k: 40, v: '每日commit' }, { k: 41, v: '每日首次Fork项目' }, ]; +export const JOB_TYPE = [{ k: 'DEBUG', v: '调试任务' }, { k: 'TRAIN', v: '训练任务' }, { k: 'INFERENCE', v: '推理任务' }, { k: 'BENCHMARK', v: '评测任务' }]; diff --git a/web_src/vuepages/pages/reward/point/utils.js b/web_src/vuepages/pages/reward/point/utils.js index b19e41b1f..d97fa4fc0 100644 --- a/web_src/vuepages/pages/reward/point/utils.js +++ b/web_src/vuepages/pages/reward/point/utils.js @@ -1,24 +1,63 @@ import { formatDate } from 'element-ui/lib/utils/date-util'; -import { SOURCE_TYPE, CONSUME_STATUS, POINT_ACTIONS } from './const'; +import { SOURCE_TYPE, CONSUME_STATUS, POINT_ACTIONS, JOB_TYPE } from './const'; const getSourceType = (key) => { const find = SOURCE_TYPE.filter(item => item.k === key); return find.length ? find[0].v : key; }; + const getConsumeStatus = (key) => { const find = CONSUME_STATUS.filter(item => item.k === key); return find.length ? find[0].v : key; }; + const getPointAction = (key) => { const find = POINT_ACTIONS.filter(item => item.k === key); return find.length ? find[0].v : key; }; +const getJobType = (key) => { + const find = JOB_TYPE.filter(item => item.k === key); + return find.length ? find[0].v : key; +}; + +const getJobTypeLink = (record, type) => { + let link = type === 'INCREASE' ? record.Action.RepoLink : '/' + record.Cloudbrain.RepoFullName; + const cloudbrain = type === 'INCREASE' ? record.Action?.Cloudbrain : record.Cloudbrain; + switch (cloudbrain?.JobType) { + case 'DEBUG': + if (cloudbrain.ComputeResource === 'CPU/GPU') { + link += `/cloudbrain/${cloudbrain.ID}`; + } else { + link += `/modelarts/notebook/${cloudbrain.ID}`; + } + break; + case 'TRAIN': + if (cloudbrain.Type === 1) { + link += `/modelarts/train-job/${cloudbrain.ID}`; + } else if (cloudbrain.Type === 0) { + link += `/cloudbrain/train-job/${cloudbrain.ID}`; + } else if (cloudbrain.Type === 2) { + link += `/grampus/train-job/${cloudbrain.ID}`; + } + break; + case 'INFERENCE': + link += `/modelarts/inference-job/${cloudbrain.ID}`; + break; + case 'BENCHMARK': + link += `/cloudbrain/benchmark/${cloudbrain.ID}`; + break; + default: + break; + }; + return link; +}; + export const getRewardPointRecordInfo = (record) => { const out = { sn: record.SerialNo, - date: formatDate(new Date(record.LastOperateDate * 1000), 'yyyy-MM-DD HH:mm:ss'), + date: formatDate(new Date(record.LastOperateDate * 1000), 'yyyy-MM-dd HH:mm:ss'), _status: record.Status, status: getConsumeStatus(record.Status) || '--', statusColor: record.Status === 'OPERATING' ? 'rgb(33, 186, 69)' : '', @@ -52,46 +91,53 @@ export const getRewardPointRecordInfo = (record) => { out.remark = `上传了数据集文件${record.Action.RefName}`; break; case 30: // 导入新模型 - 导入了新模型resnet50_qx7l + out.remark = '导入了新模型{{}}'; break; - case 32: // 完成微信扫码验证 - 首次绑定微信奖励 + case 34: // 完成微信扫码验证 - 首次绑定微信奖励 out.remark = '首次绑定微信奖励'; break; - case 33: // 每日运行云脑任务 - 创建了(CPU/GPU/NPU)类型(调试/训练/推理/评测)任务tangl202204131431995 - out.remark = `创建了{{}}类型{{}}任务${record.Action.RefName}`; + case 35: // 每日运行云脑任务 - 创建了(CPU/GPU/NPU)类型(调试/训练/推理/评测)任务tangl202204131431995 + out.remark = `创建了${record.Action?.Cloudbrain?.ComputeResource}类型${getJobType(record.Action?.Cloudbrain?.JobType)}${record.Action.RefName}`; break; - case 34: // 数据集被平台推荐 - 数据集XXX被设置为推荐数据集 - out.remark = `数据集${record.Action.RefName}被设置为推荐数据集`; + case 36: // 数据集被平台推荐 - 数据集XXX被设置为推荐数据集 + out.remark = `数据集${record.Action.Content && record.Action.Content.split('|')[1]}被设置为推荐数据集`; break; - case 35: // 提交新公开镜像 - 提交了镜像jiangxiang_ceshi_tang03 - out.remark = `提交了镜像${record.Action.RefName}`; + case 37: // 提交新公开镜像 - 提交了镜像jiangxiang_ceshi_tang03 + out.remark = `提交了镜像${record.Action.Content && record.Action.Content.split('|')[1]}`; break; - case 36: // 镜像被平台推荐 - 镜像XXX被设置为推荐镜像 - out.remark = `镜像${record.Action.RefName}被设置为推荐镜像`; + case 38: // 镜像被平台推荐 - 镜像XXX被设置为推荐镜像 + out.remark = `镜像${record.Action.Content && record.Action.Content.split('|')[1]}被设置为推荐镜像`; break; - case 37: // 首次更换头像 - 更新了头像 + case 39: // 首次更换头像 - 更新了头像 out.remark = '更新了头像'; break; - case 38: // 每日commit - 推送了xxxx分支的代码到OpenI/aiforge + case 40: // 每日commit - 推送了xxxx分支的代码到OpenI/aiforge const words = record.Action.RefName.split('/'); const branch = words[words.length - 1]; out.remark = `推送了${branch}分支的代码到${record.Action.ShortRepoFullDisplayName}`; break; - case 39: // 每日首次Fork项目 - 创建了项目OpenI/fork_aiforge + case 41: // 每日首次Fork项目 - 创建了项目OpenI/fork_aiforge out.remark = `创建了项目${record.Action.ShortRepoFullDisplayName}`; break; default: break; } } else if (record.SourceType === 'RUN_CLOUDBRAIN_TASK') { - + // } } else if (record.OperateType === 'DECREASE') { if (record.SourceType === 'ADMIN_OPERATE') { out.remark = record.Remark; } else if (record.SourceType === 'ACCOMPLISH_TASK') { - + // } else if (record.SourceType === 'RUN_CLOUDBRAIN_TASK') { - + out.taskName = `${record?.Cloudbrain?.DisplayJobName}`; + if (record?.Cloudbrain?.ComputeResource === 'CPU/GPU') { + const resourceSpec = record?.Cloudbrain?.ResourceSpec?.ResourceSpec; + out.remark = `【${getJobType(record?.Cloudbrain?.JobType)}】【${record?.Cloudbrain?.ComputeResource}】【GPU: ${resourceSpec?.gpu}, CPU: ${resourceSpec?.cpu}, 内存: ${(resourceSpec?.memMiB / 1024).toFixed(2)}GB, 共享内存: ${(resourceSpec?.shareMemMiB / 1024).toFixed(2)}GB】`; + } else { + out.remark = `【${getJobType(record?.Cloudbrain?.JobType)}】【${record?.Cloudbrain?.ComputeResource}】【${record?.Cloudbrain?.ResourceSpec.FlavorInfo.desc}】`; + } } } return out; diff --git a/web_src/vuepages/pages/reward/point/vp-point.js b/web_src/vuepages/pages/reward/point/vp-point.js index 8039d3f2c..8ef5bfa67 100644 --- a/web_src/vuepages/pages/reward/point/vp-point.js +++ b/web_src/vuepages/pages/reward/point/vp-point.js @@ -4,7 +4,6 @@ import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); import App from './vp-point.vue'; -// import App from '../manage/vp-point-manage.vue'; new Vue({ el: '#__vue-root', diff --git a/web_src/vuepages/pages/reward/point/vp-point.vue b/web_src/vuepages/pages/reward/point/vp-point.vue index 65b48f1c9..6a33ed6af 100644 --- a/web_src/vuepages/pages/reward/point/vp-point.vue +++ b/web_src/vuepages/pages/reward/point/vp-point.vue @@ -6,12 +6,10 @@

算力积分明细

@@ -78,13 +76,17 @@ + width="120"> - + + @@ -109,8 +111,7 @@ \ No newline at end of file diff --git a/templates/repo/cloudbrain/new.tmpl b/templates/repo/cloudbrain/new.tmpl index 295fe0435..955457eef 100755 --- a/templates/repo/cloudbrain/new.tmpl +++ b/templates/repo/cloudbrain/new.tmpl @@ -104,7 +104,26 @@ top: 14px; z-index: 2; */ } + .inline.field { + padding-left: 12rem !important; + } + + .inline.field>label { + width: 120px !important; + text-align: right; + } + .inline.field .dropdown.selection { + width: 60% !important; + } + + .width70 { + width: 60% !important; + } + + .inline.field input { + width: 40% !important; + }
@@ -119,170 +138,176 @@
{{template "repo/header" .}} -
-
- - {{template "base/alert" .}} -
-

-
+
+ + {{template "base/alert" .}} +

+ {{.i18n.Tr "repo.cloudbrain.new"}} +

+ +
{{.CsrfTokenHtml}} -

- {{.i18n.Tr "repo.cloudbrain.new"}} -

-
-
- - -
-
- - +
+ + +
+
+ + +
-
- - -
+
+ + +
-
- - -
- -
- -
-
- -
- - -
-
- - +
+ + +
+ +
+
+
+ +
+ + +
+
+ + +
-
+
-
-
+
+
+
+ +
+ + +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} +
- -
- - -
+
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ -
- - - {{.i18n.Tr "repo.cloudbrain.cancel"}} -
+
+ + + {{.i18n.Tr "repo.cloudbrain.cancel"}}
- +
-
+
{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/repo/cloudbrain/trainjob/new.tmpl b/templates/repo/cloudbrain/trainjob/new.tmpl index adf3adf9b..9b10897dd 100755 --- a/templates/repo/cloudbrain/trainjob/new.tmpl +++ b/templates/repo/cloudbrain/trainjob/new.tmpl @@ -25,6 +25,10 @@ margin-left: -2px; } + .width485 { + width: 48.5% !important; + } + .width85 { width: 85% !important; margin-left: 10.5rem !important; @@ -247,13 +251,22 @@
- {{range .train_resource_specs}} - + {{end}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
@@ -493,4 +506,19 @@ send_run_para() validate() }) + + ;(function() { + $('#cloudbrain_resource_spec').on('change', function(e) { + var cloudbrain_resource_spec_blance_tip_el = $('.cloudbrain_resource_spec_blance_tip'); + var val = $(this).val(); + var blance = $(this).attr('blance'); + var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (unitPrice == 0) { + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); + } else { + var canUseTime = Number(blance) / Number(unitPrice); + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').text(canUseTime.toFixed(2)).parent().show(); + } + }).trigger('change'); + })(); \ No newline at end of file diff --git a/templates/repo/modelarts/inferencejob/new.tmpl b/templates/repo/modelarts/inferencejob/new.tmpl index 90a7c900d..84b6d3951 100644 --- a/templates/repo/modelarts/inferencejob/new.tmpl +++ b/templates/repo/modelarts/inferencejob/new.tmpl @@ -222,11 +222,21 @@
        - {{range .flavor_infos}} - + {{end}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
@@ -463,4 +473,19 @@ get_name() validate() }) + + ;(function() { + $('#trainjob-flavor').on('change', function(e) { + var cloudbrain_resource_spec_blance_tip_el = $('.cloudbrain_resource_spec_blance_tip'); + var val = $(this).val(); + var blance = $(this).attr('blance'); + var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (unitPrice == 0) { + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); + } else { + var canUseTime = Number(blance) / Number(unitPrice); + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').text(canUseTime.toFixed(2)).parent().show(); + } + }).trigger('change'); + })(); diff --git a/templates/repo/modelarts/notebook/new.tmpl b/templates/repo/modelarts/notebook/new.tmpl index 4e2b3951d..38ac6b01f 100755 --- a/templates/repo/modelarts/notebook/new.tmpl +++ b/templates/repo/modelarts/notebook/new.tmpl @@ -3,6 +3,26 @@ .inline.required.field.cloudbrain_benchmark { display: none; } +.inline.field { + padding-left: 12rem !important; +} + +.inline.field>label { + width: 120px !important; + text-align: right; +} + +.inline.field .dropdown.selection { + width: 60% !important; +} + +.width70 { + width: 60% !important; +} + +.inline.field input { + width: 40% !important; +}
@@ -16,87 +36,92 @@
{{template "repo/header" .}} -
-
- - {{template "base/alert" .}} -
-

-
+
+ + {{template "base/alert" .}} +

+ {{.i18n.Tr "repo.cloudbrain.new"}} +

+ +
{{.CsrfTokenHtml}} -

- {{.i18n.Tr "repo.cloudbrain.new"}} -

-
- -
- - -
-
- - +
+ + +
+
+ + +
-
- - -
-
+
+ + +
+
-
+
- -
- - -
- -
- - -
-
- - - {{.i18n.Tr "repo.cloudbrain.cancel"}} + +
+ + +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} +
+ +
+ + +
+
+ + + {{.i18n.Tr "repo.cloudbrain.cancel"}} +
@@ -158,4 +183,19 @@ } }); }); + + ;(function() { + $('#cloudbrain_flavor').on('change', function(e) { + var cloudbrain_resource_spec_blance_tip_el = $('.cloudbrain_resource_spec_blance_tip'); + var val = $(this).val(); + var blance = $(this).attr('blance'); + var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (unitPrice == 0) { + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); + } else { + var canUseTime = Number(blance) / Number(unitPrice); + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').text(canUseTime.toFixed(2)).parent().show(); + } + }).trigger('change'); + })(); diff --git a/templates/repo/modelarts/trainjob/new.tmpl b/templates/repo/modelarts/trainjob/new.tmpl index 5022bd41b..ff9f35b51 100755 --- a/templates/repo/modelarts/trainjob/new.tmpl +++ b/templates/repo/modelarts/trainjob/new.tmpl @@ -247,11 +247,21 @@
- {{range .flavor_infos}} - + {{end}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
@@ -503,4 +513,19 @@ send_run_para() validate() }) + + ;(function() { + $('#trainjob-flavor').on('change', function(e) { + var cloudbrain_resource_spec_blance_tip_el = $('.cloudbrain_resource_spec_blance_tip'); + var val = $(this).val(); + var blance = $(this).attr('blance'); + var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (unitPrice == 0) { + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); + } else { + var canUseTime = Number(blance) / Number(unitPrice); + cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').text(canUseTime.toFixed(2)).parent().show(); + } + }).trigger('change'); + })(); \ No newline at end of file From 24afb0065ced24648411ec2dee9fe9643b68c897 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 13 Jul 2022 14:25:08 +0800 Subject: [PATCH 068/283] #2225 update --- modules/context/point.go | 2 ++ routers/reward/point/point.go | 2 +- services/reward/notify.go | 10 +++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/context/point.go b/modules/context/point.go index 9fbff61be..8fd4724ee 100644 --- a/modules/context/point.go +++ b/modules/context/point.go @@ -1,6 +1,7 @@ package context import ( + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/reward/point/account" "gitea.com/macaron/macaron" ) @@ -14,6 +15,7 @@ func PointAccount() macaron.Handler { return } ctx.Data["PointAccount"] = a + ctx.Data["CloudBrainPaySwitch"] = setting.CloudBrainPaySwitch ctx.Next() } } diff --git a/routers/reward/point/point.go b/routers/reward/point/point.go index a8ae00ce4..7b3e0fe49 100644 --- a/routers/reward/point/point.go +++ b/routers/reward/point/point.go @@ -71,7 +71,7 @@ func GetPointRecordList(ctx *context.Context) { func OperatePointAccountBalance(ctx *context.Context, req models.AdminRewardOperateReq) { req.RewardType = models.RewardTypePoint - if req.OperateType.Name() == "" { + if req.OperateType.Name() == "" || req.Remark == "" { ctx.JSON(http.StatusOK, "param error") return } diff --git a/services/reward/notify.go b/services/reward/notify.go index 875dde199..4f3190d67 100644 --- a/services/reward/notify.go +++ b/services/reward/notify.go @@ -11,7 +11,15 @@ import ( "time" ) -func NotifyRewardOperation(userId, amount int64, rewardType models.RewardType, operateType models.RewardOperateType) { +func NotifyRewardOperation(userId, amount int64, sourceType models.SourceType, rewardType models.RewardType, operateType models.RewardOperateType) { + switch sourceType { + case models.SourceTypeRunCloudbrainTask: + return + case models.SourceTypeAdminOperate: + if operateType == models.OperateTypeDecrease { + return + } + } data := &models.UserRewardOperationRedis{ UserId: userId, Amount: amount, From 252064d4100011fce03b2649df40625f763ee650 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 13 Jul 2022 14:39:38 +0800 Subject: [PATCH 069/283] #2225 update --- services/reward/notify.go | 4 ---- services/reward/operator.go | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/services/reward/notify.go b/services/reward/notify.go index 4f3190d67..4db218537 100644 --- a/services/reward/notify.go +++ b/services/reward/notify.go @@ -15,10 +15,6 @@ func NotifyRewardOperation(userId, amount int64, sourceType models.SourceType, r switch sourceType { case models.SourceTypeRunCloudbrainTask: return - case models.SourceTypeAdminOperate: - if operateType == models.OperateTypeDecrease { - return - } } data := &models.UserRewardOperationRedis{ UserId: userId, diff --git a/services/reward/operator.go b/services/reward/operator.go index f32024688..4e1d53b75 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -86,7 +86,7 @@ func Operate(ctx *models.RewardOperateContext) error { } UpdateRewardRecordToFinalStatus(ctx.SourceType.Name(), ctx.RequestId, models.OperateStatusSucceeded) - NotifyRewardOperation(ctx.TargetUserId, ctx.Reward.Amount, ctx.Reward.Type, ctx.OperateType) + NotifyRewardOperation(ctx.TargetUserId, ctx.Reward.Amount, ctx.SourceType, ctx.Reward.Type, ctx.OperateType) return nil } From 0e9b41328393a1c618c2742c71d51a8360636e7f Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 13 Jul 2022 17:25:39 +0800 Subject: [PATCH 070/283] #2225 update --- routers/repo/cloudbrain.go | 1 - 1 file changed, 1 deletion(-) diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 031ae2617..20360084c 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -2,7 +2,6 @@ package repo import ( "bufio" - "code.gitea.io/gitea/modules/grampus" "code.gitea.io/gitea/services/reward/point/account" "encoding/json" "errors" From 93ae27de092ff1d2eea925966c98e9533b392aa6 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 13 Jul 2022 17:32:10 +0800 Subject: [PATCH 071/283] #2225 update --- modules/templates/helper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 857e365f8..797ccdb2e 100755 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -788,7 +788,7 @@ func GetRefName(ref string) string { return reg.ReplaceAllString(ref, "") } -func MB2GB(size int64) string { +func MB2GB(size int) string { s := strconv.FormatFloat(float64(size)/float64(1024), 'f', 2, 64) for strings.HasSuffix(s, "0") { s = strings.TrimSuffix(s, "0") From b5ead01ff8dc80e7f2526fa0e5b717c917451b31 Mon Sep 17 00:00:00 2001 From: chenshihai Date: Wed, 13 Jul 2022 17:39:11 +0800 Subject: [PATCH 072/283] =?UTF-8?q?=E7=AE=97=E5=8A=9B=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/repo/cloudbrain/benchmark/new.tmpl | 25 +++++++++++++------ templates/repo/cloudbrain/new.tmpl | 13 +++++++--- templates/repo/cloudbrain/trainjob/new.tmpl | 13 +++++++--- .../repo/modelarts/inferencejob/new.tmpl | 11 +++++--- templates/repo/modelarts/notebook/new.tmpl | 11 +++++--- templates/repo/modelarts/trainjob/new.tmpl | 13 +++++++--- web_src/js/features/notification.js | 5 +++- web_src/vuepages/langs/config/en-US.js | 4 +-- web_src/vuepages/langs/config/zh-CN.js | 4 +-- .../vuepages/pages/reward/point/vp-point.vue | 4 +-- 10 files changed, 70 insertions(+), 33 deletions(-) diff --git a/templates/repo/cloudbrain/benchmark/new.tmpl b/templates/repo/cloudbrain/benchmark/new.tmpl index d2dae1a50..ac0481b5f 100755 --- a/templates/repo/cloudbrain/benchmark/new.tmpl +++ b/templates/repo/cloudbrain/benchmark/new.tmpl @@ -126,22 +126,26 @@ {{template "custom/select_dataset_train" .}}
- + {{if .CloudBrainPaySwitch}}
{{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} {{$.i18n.Tr "points.points_acquisition_instructions"}} + {{end}}
@@ -226,15 +230,18 @@
+ {{if .CloudBrainPaySwitch}}
{{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} @@ -242,6 +249,7 @@ {{$.i18n.Tr "points.points_acquisition_instructions"}}
+ {{end}}
@@ -375,6 +383,7 @@ var val = $(this).val(); var blance = $(this).attr('blance'); var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (!blance || !unitPrice) return; if (unitPrice == 0) { cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); } else { diff --git a/templates/repo/cloudbrain/new.tmpl b/templates/repo/cloudbrain/new.tmpl index 955457eef..c3fccbf34 100755 --- a/templates/repo/cloudbrain/new.tmpl +++ b/templates/repo/cloudbrain/new.tmpl @@ -243,16 +243,19 @@
- + {{if .CloudBrainPaySwitch}}
{{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} @@ -260,6 +263,7 @@ {{$.i18n.Tr "points.points_acquisition_instructions"}}
+ {{end}}
@@ -405,6 +409,7 @@ var val = $(this).val(); var blance = $(this).attr('blance'); var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (!blance || !unitPrice) return; if (unitPrice == 0) { cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); } else { diff --git a/templates/repo/cloudbrain/trainjob/new.tmpl b/templates/repo/cloudbrain/trainjob/new.tmpl index 9b10897dd..de7ca766c 100755 --- a/templates/repo/cloudbrain/trainjob/new.tmpl +++ b/templates/repo/cloudbrain/trainjob/new.tmpl @@ -252,14 +252,17 @@
+ {{if .CloudBrainPaySwitch}}
{{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} @@ -267,6 +270,7 @@ {{$.i18n.Tr "points.points_acquisition_instructions"}}
+ {{end}}
@@ -513,6 +517,7 @@ var val = $(this).val(); var blance = $(this).attr('blance'); var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (!blance || !unitPrice) return; if (unitPrice == 0) { cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); } else { diff --git a/templates/repo/modelarts/inferencejob/new.tmpl b/templates/repo/modelarts/inferencejob/new.tmpl index 84b6d3951..f6fc6afd9 100644 --- a/templates/repo/modelarts/inferencejob/new.tmpl +++ b/templates/repo/modelarts/inferencejob/new.tmpl @@ -222,14 +222,17 @@
        - {{range .flavor_infos}} - {{end}} + {{if .CloudBrainPaySwitch}}
{{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} @@ -237,6 +240,7 @@ {{$.i18n.Tr "points.points_acquisition_instructions"}}
+ {{end}}
@@ -480,6 +484,7 @@ var val = $(this).val(); var blance = $(this).attr('blance'); var unitPrice = $(this).find('option:selected').attr('unitprice'); + if (!blance || !unitPrice) return; if (unitPrice == 0) { cloudbrain_resource_spec_blance_tip_el.find('.can-use-time').parent().hide(); } else { diff --git a/templates/repo/modelarts/notebook/new.tmpl b/templates/repo/modelarts/notebook/new.tmpl index 38ac6b01f..2d4a821c6 100755 --- a/templates/repo/modelarts/notebook/new.tmpl +++ b/templates/repo/modelarts/notebook/new.tmpl @@ -92,13 +92,16 @@
-->
- {{range .flavors}} - {{end}} + {{if .CloudBrainPaySwitch}}
{{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} @@ -106,6 +109,7 @@ {{$.i18n.Tr "points.points_acquisition_instructions"}}
+ {{end}}
- + {{if .CloudBrainPaySwitch}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
+ {{end}}
@@ -518,8 +527,8 @@ }) ;(function() { var SPECS = {{ .inference_specs }}; - var showPoint = true; - renderSpecsSelect($('#__specs__'), SPECS, showPoint, { + var showPoint = {{ .CloudBrainPaySwitch }}; + window.renderSpecsSelect($('#__specs__'), SPECS, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, point_hr: {{$.i18n.Tr "cloudbrain.point_hr"}}, diff --git a/templates/repo/cloudbrain/inference/show.tmpl b/templates/repo/cloudbrain/inference/show.tmpl index 1d19627dd..157d29055 100644 --- a/templates/repo/cloudbrain/inference/show.tmpl +++ b/templates/repo/cloudbrain/inference/show.tmpl @@ -618,7 +618,7 @@ ;(function() { var SPEC = {{ .Spec }}; - var showPoint = true; + var showPoint = false; var specStr = window.renderSpecStr(SPEC, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, diff --git a/templates/repo/cloudbrain/new.tmpl b/templates/repo/cloudbrain/new.tmpl index cac971eaf..d02f50d68 100755 --- a/templates/repo/cloudbrain/new.tmpl +++ b/templates/repo/cloudbrain/new.tmpl @@ -146,8 +146,18 @@ + {{if .CloudBrainPaySwitch}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
+ {{end}}
@@ -303,13 +313,13 @@ ;(function() { var SPECS = {{ .debug_specs }}; - var showPoint = true; + var showPoint = {{ .CloudBrainPaySwitch }}; window.renderSpecsSelect($('#__specs__'), SPECS, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, point_hr: {{$.i18n.Tr "cloudbrain.point_hr"}}, memory: {{$.i18n.Tr "cloudbrain.memory"}}, shared_memory: {{$.i18n.Tr "cloudbrain.shared_memory"}}, - }); + }); })(); \ No newline at end of file diff --git a/templates/repo/cloudbrain/show.tmpl b/templates/repo/cloudbrain/show.tmpl index 8096e1e8a..b86a5b9bd 100755 --- a/templates/repo/cloudbrain/show.tmpl +++ b/templates/repo/cloudbrain/show.tmpl @@ -595,7 +595,7 @@ } ;(function() { var SPEC = {{ .Spec }}; - var showPoint = true; + var showPoint = false; var specStr = window.renderSpecStr(SPEC, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, diff --git a/templates/repo/cloudbrain/trainjob/new.tmpl b/templates/repo/cloudbrain/trainjob/new.tmpl index 9d680cd70..1fba6df8e 100755 --- a/templates/repo/cloudbrain/trainjob/new.tmpl +++ b/templates/repo/cloudbrain/trainjob/new.tmpl @@ -259,8 +259,18 @@
+ {{if .CloudBrainPaySwitch}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
+ {{end}}
@@ -435,8 +445,8 @@ }) ;(function() { var SPECS = {{ .train_specs }}; - var showPoint = true; - renderSpecsSelect($('#__specs__'), SPECS, showPoint, { + var showPoint = {{ .CloudBrainPaySwitch }}; + window.renderSpecsSelect($('#__specs__'), SPECS, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, point_hr: {{$.i18n.Tr "cloudbrain.point_hr"}}, diff --git a/templates/repo/cloudbrain/trainjob/show.tmpl b/templates/repo/cloudbrain/trainjob/show.tmpl index 1dba0b7f0..bcebf11ac 100644 --- a/templates/repo/cloudbrain/trainjob/show.tmpl +++ b/templates/repo/cloudbrain/trainjob/show.tmpl @@ -984,7 +984,7 @@ ;(function() { var SPEC = {{ .Spec }}; - var showPoint = true; + var showPoint = false; var specStr = window.renderSpecStr(SPEC, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, diff --git a/templates/repo/grampus/trainjob/gpu/new.tmpl b/templates/repo/grampus/trainjob/gpu/new.tmpl index 6eb2b49fd..5b3e3cf70 100755 --- a/templates/repo/grampus/trainjob/gpu/new.tmpl +++ b/templates/repo/grampus/trainjob/gpu/new.tmpl @@ -206,7 +206,16 @@
-->
- + + {{if .CloudBrainPaySwitch}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
+ {{end}}
@@ -386,8 +395,8 @@ ;(function() { var SPECS = {{ .Specs }}; - var showPoint = true; - renderSpecsSelect($('#__specs__'), SPECS, showPoint, { + var showPoint = {{ .CloudBrainPaySwitch }}; + window.renderSpecsSelect($('#__specs__'), SPECS, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, point_hr: {{$.i18n.Tr "cloudbrain.point_hr"}}, diff --git a/templates/repo/grampus/trainjob/npu/new.tmpl b/templates/repo/grampus/trainjob/npu/new.tmpl index 6849528dc..df40699b8 100755 --- a/templates/repo/grampus/trainjob/npu/new.tmpl +++ b/templates/repo/grampus/trainjob/npu/new.tmpl @@ -218,7 +218,16 @@
-->
- + + {{if .CloudBrainPaySwitch}} +
+ {{$.i18n.Tr "points.balance_of_points"}}{{.PointAccount.Balance}}{{$.i18n.Tr "points.points"}}{{$.i18n.Tr "points.expected_time"}}{{$.i18n.Tr "points.hours"}} + + + {{$.i18n.Tr "points.points_acquisition_instructions"}} + +
+ {{end}}
@@ -407,8 +416,8 @@ ;(function() { var SPECS = {{ .Specs }}; - var showPoint = true; - renderSpecsSelect($('#__specs__'), SPECS, showPoint, { + var showPoint = {{ .CloudBrainPaySwitch }}; + window.renderSpecsSelect($('#__specs__'), SPECS, showPoint, { gpu_memory: {{$.i18n.Tr "cloudbrain.gpu_memory"}}, free: {{$.i18n.Tr "cloudbrain.free"}}, point_hr: {{$.i18n.Tr "cloudbrain.point_hr"}}, diff --git a/templates/repo/grampus/trainjob/show.tmpl b/templates/repo/grampus/trainjob/show.tmpl index 6e9e0d6dc..7e7fd86ea 100755 --- a/templates/repo/grampus/trainjob/show.tmpl +++ b/templates/repo/grampus/trainjob/show.tmpl @@ -633,7 +633,7 @@ \ No newline at end of file diff --git a/templates/repo/cloudbrain/show.tmpl b/templates/repo/cloudbrain/show.tmpl index b86a5b9bd..6cd977af2 100755 --- a/templates/repo/cloudbrain/show.tmpl +++ b/templates/repo/cloudbrain/show.tmpl @@ -346,9 +346,7 @@ -
- {{$.resource_type}} -
+
@@ -604,6 +602,6 @@ shared_memory: {{$.i18n.Tr "cloudbrain.shared_memory"}}, }); $('td.ti-text-form-content.spec div').text(specStr); - $('td.ti-text-form-content.resorce_type div').text(getListValueWithKey(ACC_CARD_TYPE, SPEC.AccCardType)); + SPEC && $('td.ti-text-form-content.resorce_type div').text(getListValueWithKey(ACC_CARD_TYPE, SPEC.AccCardType)); })(); \ No newline at end of file diff --git a/templates/repo/cloudbrain/trainjob/show.tmpl b/templates/repo/cloudbrain/trainjob/show.tmpl index bcebf11ac..5ffce27cc 100644 --- a/templates/repo/cloudbrain/trainjob/show.tmpl +++ b/templates/repo/cloudbrain/trainjob/show.tmpl @@ -360,9 +360,7 @@ -
- {{$.resource_type}} -
+
@@ -371,9 +369,7 @@ -
- {{$.i18n.Tr "cloudbrain.gpu_num"}}:{{$.GpuNum}},{{$.i18n.Tr "cloudbrain.cpu_num"}}:{{$.CpuNum}},{{$.i18n.Tr "cloudbrain.memory"}}(MB):{{$.MemMiB}},{{$.i18n.Tr "cloudbrain.shared_memory"}}(MB):{{$.ShareMemMiB}} -
+
@@ -993,6 +989,6 @@ shared_memory: {{$.i18n.Tr "cloudbrain.shared_memory"}}, }); $('td.ti-text-form-content.spec div').text(specStr); - $('td.ti-text-form-content.resorce_type div').text(getListValueWithKey(ACC_CARD_TYPE, SPEC.AccCardType)); + SPEC && $('td.ti-text-form-content.resorce_type div').text(getListValueWithKey(ACC_CARD_TYPE, SPEC.AccCardType)); })(); \ No newline at end of file diff --git a/templates/repo/grampus/trainjob/show.tmpl b/templates/repo/grampus/trainjob/show.tmpl index 7e7fd86ea..c76dbc940 100755 --- a/templates/repo/grampus/trainjob/show.tmpl +++ b/templates/repo/grampus/trainjob/show.tmpl @@ -358,9 +358,7 @@ -
- {{.FlavorName}} -
+
diff --git a/templates/repo/modelarts/inferencejob/show.tmpl b/templates/repo/modelarts/inferencejob/show.tmpl index 3f8fda531..619a2acba 100644 --- a/templates/repo/modelarts/inferencejob/show.tmpl +++ b/templates/repo/modelarts/inferencejob/show.tmpl @@ -424,9 +424,7 @@ td, th { -
- {{.FlavorName}} -
+
@@ -543,6 +541,5 @@ $(document).ready(function(){ shared_memory: {{$.i18n.Tr "cloudbrain.shared_memory"}}, }); $('td.ti-text-form-content.spec div').text(specStr); - // $('td.ti-text-form-content.resorce_type').text(getListValueWithKey(ACC_CARD_TYPE, SPEC.AccCardType)); })(); diff --git a/templates/repo/modelarts/notebook/show.tmpl b/templates/repo/modelarts/notebook/show.tmpl index 5353f7a52..2a22392cb 100755 --- a/templates/repo/modelarts/notebook/show.tmpl +++ b/templates/repo/modelarts/notebook/show.tmpl @@ -368,9 +368,7 @@ -
- {{$.resource_spec}} -
+
@@ -504,6 +502,5 @@ shared_memory: {{$.i18n.Tr "cloudbrain.shared_memory"}}, }); $('td.ti-text-form-content.spec div').text(specStr); - $('td.ti-text-form-content.resorce_type div').text(getListValueWithKey(ACC_CARD_TYPE, SPEC.AccCardType)); })(); diff --git a/templates/repo/modelarts/trainjob/show.tmpl b/templates/repo/modelarts/trainjob/show.tmpl index e10afc38a..ddbbad3f0 100755 --- a/templates/repo/modelarts/trainjob/show.tmpl +++ b/templates/repo/modelarts/trainjob/show.tmpl @@ -398,9 +398,7 @@ -
- {{.FlavorName}} -
+
diff --git a/web_src/js/standalone/specsuse.js b/web_src/js/standalone/specsuse.js index 97a4647fe..99f5445a4 100644 --- a/web_src/js/standalone/specsuse.js +++ b/web_src/js/standalone/specsuse.js @@ -9,6 +9,7 @@ window.getListValueWithKey = (list, key, k = 'k', v = 'v', defaultV = '') => { }; window.renderSpecStr = (spec, showPoint, langObj) => { + if (!spec) return ''; var ngpu = `${spec.ComputeResource}: ${spec.AccCardsNum + '*' + getListValueWithKey(ACC_CARD_TYPE, spec.AccCardType)}`; var gpuMemStr = spec.GPUMemGiB != 0 ? `${langObj.gpu_memory}: ${spec.GPUMemGiB}GB, ` : ''; var sharedMemStr = spec.ShareMemGiB != 0 ? `, ${langObj.shared_memory}: ${spec.ShareMemGiB}GB` : ''; From 811e02e2eeddc601ff29d4e88ccffdaa0c09b55c Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 31 Aug 2022 15:29:57 +0800 Subject: [PATCH 090/283] #2701 update point serialNo --- services/reward/operator.go | 17 ----------------- services/reward/serial.go | 9 +++++---- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/services/reward/operator.go b/services/reward/operator.go index c9f00b1bf..b66810c70 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -276,22 +276,5 @@ func generateOperateSerialNo(operateType models.RewardOperateType, rewardType mo return "", err } - - switch operateType { - case models.OperateTypeIncrease: - s += "1" - case models.OperateTypeDecrease: - s += "2" - default: - s += "9" - } - - switch rewardType { - case models.RewardTypePoint: - s += "1" - default: - s += "9" - } - return s, nil } diff --git a/services/reward/serial.go b/services/reward/serial.go index b6a47bbc3..349da1266 100644 --- a/services/reward/serial.go +++ b/services/reward/serial.go @@ -11,13 +11,14 @@ import ( func GetSerialNoByRedis() (string, error) { now := time.Now() - n, err := redis_client.IncrBy(redis_key.RewardSerialCounter(now), 1) + r := int64(rand.Intn(4)) + 1 + n, err := redis_client.IncrBy(redis_key.RewardSerialCounter(now), r) if err != nil { log.Error("GetSerialNoByRedis RewardSerialCounter error. %v", err) return "", err } - if n == 1 { - redis_client.Expire(redis_key.RewardSerialCounter(now), 5*time.Minute) + if n == r { + redis_client.Expire(redis_key.RewardSerialCounter(now), 2*time.Minute) } - return now.Format("200601021504") + fmt.Sprint(rand.Intn(10)) + fmt.Sprintf("%02d", n), nil + return now.Format("200601021504") + fmt.Sprintf("%03d", n) + fmt.Sprint(rand.Intn(10)), nil } From 283d9045aa080a70ac3dfac1f04100bb10de6ae8 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Wed, 31 Aug 2022 15:34:43 +0800 Subject: [PATCH 091/283] #2701 update point serialNo --- services/reward/operator.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/reward/operator.go b/services/reward/operator.go index b66810c70..b9d8c8d59 100644 --- a/services/reward/operator.go +++ b/services/reward/operator.go @@ -116,7 +116,7 @@ func isHandled(sourceType string, requestId string, operateType string) (bool, e } func initRewardOperateRecord(ctx *models.RewardOperateContext) (string, error) { - sn, err := generateOperateSerialNo(ctx.OperateType, ctx.Reward.Type) + sn, err := generateOperateSerialNo() if err != nil { log.Error("generateOperateSerialNo error. %v", err) return "", err @@ -145,7 +145,7 @@ func initRewardOperateRecord(ctx *models.RewardOperateContext) (string, error) { } func createPeriodicRewardOperateRecord(ctx *models.StartPeriodicTaskOpts) (string, error) { - sn, err := generateOperateSerialNo(ctx.OperateType, ctx.RewardType) + sn, err := generateOperateSerialNo() if err != nil { log.Error("createPeriodic generateOperateSerialNo error. %v", err) return "", err @@ -269,7 +269,7 @@ func StopPeriodicTask(sourceType models.SourceType, sourceId string, operateType return models.StopPeriodicTask(task.ID, task.OperateSerialNo, now) } -func generateOperateSerialNo(operateType models.RewardOperateType, rewardType models.RewardType) (string, error) { +func generateOperateSerialNo() (string, error) { s, err := GetSerialNoByRedis() if err != nil { log.Error("generateOperateSerialNo error. %v", err) From 7c6a21a6cfbdf8ed32a3079b00e7c2fa2a2e934c Mon Sep 17 00:00:00 2001 From: liuzx Date: Thu, 1 Sep 2022 12:04:48 +0800 Subject: [PATCH 092/283] add lock to stop same task --- routers/repo/cloudbrain.go | 38 ++++++++++++++++++-------------- routers/repo/grampus.go | 21 ++++++++---------- routers/repo/modelarts.go | 45 ++++++++++++++------------------------ 3 files changed, 48 insertions(+), 56 deletions(-) diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 28d6b4361..34917d2cd 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -30,8 +30,8 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/modelarts" - "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/redis/redis_lock" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" @@ -100,9 +100,6 @@ func jobNamePrefixValid(s string) string { } func cloudBrainNewDataPrepare(ctx *context.Context) error { - var taskJobNameKey = ctx.Query("taskJobNameKey") - redis_client.Del(taskJobNameKey) - ctx.Data["PageIsCloudBrain"] = true t := time.Now() var displayJobName = jobNamePrefixValid(cutString(ctx.User.Name, 5)) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[5:] @@ -252,14 +249,14 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { tpl = tplCloudBrainTrainJobNew } - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), jobType, displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tpl, &form) return } + defer lock.UnLock() tasks, err := models.GetCloudbrainsByDisplayJobName(repo.ID, jobType, displayJobName) if err == nil { @@ -439,14 +436,14 @@ func CloudBrainInferenceJobCreate(ctx *context.Context, form auth.CreateCloudBra repo := ctx.Repo.Repository tpl := tplCloudBrainInferenceJobNew - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeInference), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), jobType, displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tpl, &form) return } + defer lock.UnLock() ckptUrl := setting.Attachment.Minio.RealPath + form.TrainUrl + form.CkptName log.Info("ckpt url:" + ckptUrl) @@ -2319,14 +2316,14 @@ func BenchMarkAlgorithmCreate(ctx *context.Context, form auth.CreateCloudBrainFo ctx.Data["benchmarkTypeID"] = benchmarkTypeID ctx.Data["benchmark_child_types_id_hidden"] = benchmarkChildTypeID - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeBenchmark), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), form.JobType, displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tplCloudBrainBenchmarkNew, &form) return } + defer lock.UnLock() tasks, err := models.GetCloudbrainsByDisplayJobName(repo.ID, string(models.JobTypeBenchmark), displayJobName) if err == nil { @@ -2515,6 +2512,15 @@ func ModelBenchmarkCreate(ctx *context.Context, form auth.CreateCloudBrainForm) tpl := tplCloudBrainBenchmarkNew command := cloudbrain.GetCloudbrainDebugCommand() + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), jobType, displayJobName)) + isOk := lock.Lock(60 * time.Second) + if !isOk { + log.Error("The task have been processed", ctx.Data["MsgID"]) + ctx.RenderWithErr("The task have been processed, please wait a minute", tpl, &form) + return + } + defer lock.UnLock() + tasks, err := models.GetCloudbrainsByDisplayJobName(repo.ID, jobType, displayJobName) if err == nil { if len(tasks) != 0 { diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index ab86f9e04..61d0156d1 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -17,8 +17,8 @@ import ( "code.gitea.io/gitea/modules/grampus" "code.gitea.io/gitea/modules/modelarts" "code.gitea.io/gitea/modules/notification" - "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/redis/redis_lock" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "github.com/unknwon/com" @@ -63,9 +63,6 @@ func GrampusTrainJobNPUNew(ctx *context.Context) { } func grampusTrainJobNewDataPrepare(ctx *context.Context, processType string) error { - var taskJobNameKey = ctx.Query("taskJobNameKey") - redis_client.Del(taskJobNameKey) - ctx.Data["PageIsCloudBrain"] = true t := time.Now() @@ -214,14 +211,14 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain flavorName := form.FlavorName image := strings.TrimSpace(form.Image) - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tplGrampusTrainJobGPUNew, &form) return } + defer lock.UnLock() if !jobNamePattern.MatchString(displayJobName) { grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) @@ -415,14 +412,14 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain versionCount := modelarts.VersionCountOne engineName := form.EngineName - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tplGrampusTrainJobNPUNew, &form) return } + defer lock.UnLock() if !jobNamePattern.MatchString(displayJobName) { grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index f50c6b132..9851b3842 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -25,8 +25,8 @@ import ( "code.gitea.io/gitea/modules/modelarts" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/obs" - "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" + "code.gitea.io/gitea/modules/redis/redis_lock" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" @@ -125,9 +125,6 @@ func NotebookNew(ctx *context.Context) { } func notebookNewDataPrepare(ctx *context.Context) error { - var taskJobNameKey = ctx.Query("taskJobNameKey") - redis_client.Del(taskJobNameKey) - ctx.Data["PageIsCloudBrain"] = true t := time.Now() var displayJobName = jobNamePrefixValid(cutString(ctx.User.Name, 5)) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[5:] @@ -213,14 +210,14 @@ func Notebook2Create(ctx *context.Context, form auth.CreateModelArtsNotebookForm imageId := form.ImageId repo := ctx.Repo.Repository - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeDebug), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeDebug), displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tplModelArtsNotebookNew, &form) return } + defer lock.UnLock() count, err := models.GetCloudbrainNotebookCountByUserID(ctx.User.ID) if err != nil { @@ -816,8 +813,6 @@ func setSpecBySpecialPoolConfig(ctx *context.Context, jobType string) { } func trainJobErrorNewDataPrepare(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) error { - var taskJobNameKey = ctx.Query("taskJobNameKey") - redis_client.Del(taskJobNameKey) ctx.Data["PageIsCloudBrain"] = true //can, err := canUserCreateTrainJob(ctx.User.ID) @@ -1013,9 +1008,6 @@ func trainJobNewVersionDataPrepare(ctx *context.Context) error { } func versionErrorDataPrepare(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) error { - var taskJobNameKey = ctx.Query("taskJobNameKey") - redis_client.Del(taskJobNameKey) - ctx.Data["PageIsCloudBrain"] = true var jobID = ctx.Params(":jobid") // var versionName = ctx.Params(":version-name") @@ -1134,14 +1126,14 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) VersionCount := modelarts.VersionCountOne EngineName := form.EngineName - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tplModelArtsTrainJobIndex, &form) return } + defer lock.UnLock() count, err := models.GetCloudbrainTrainJobCountByUserID(ctx.User.ID) if err != nil { @@ -1456,14 +1448,14 @@ func TrainJobCreateVersion(ctx *context.Context, form auth.CreateModelArtsTrainJ EngineName := form.EngineName isLatestVersion := modelarts.IsLatestVersion - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeTrain), displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tplModelArtsTrainJobVersionNew, &form) return } + defer lock.UnLock() canNewJob, _ := canUserCreateTrainJobVersion(ctx, latestTask.UserID) if !canNewJob { @@ -2058,14 +2050,14 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference ckptUrl := "/" + form.TrainUrl + form.CkptName log.Info("ckpt url:" + ckptUrl) - taskJobNameKey := redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeInference), displayJobName) - isOk, err := redis_client.Setnx(taskJobNameKey, "", 5*time.Second) - ctx.Data["taskJobNameKey"] = taskJobNameKey + lock := redis_lock.NewDistributeLock(redis_key.CloudbrainBindingJobNameKey(fmt.Sprint(repo.ID), string(models.JobTypeInference), displayJobName)) + isOk := lock.Lock(60 * time.Second) if !isOk { - log.Error("The task have been processed:%v", err, ctx.Data["MsgID"]) + log.Error("The task have been processed", ctx.Data["MsgID"]) ctx.RenderWithErr("The task have been processed, please wait a minute", tplModelArtsInferenceJobNew, &form) return } + defer lock.UnLock() count, err := models.GetCloudbrainInferenceJobCountByUserID(ctx.User.ID) if err != nil { @@ -2468,9 +2460,6 @@ func inferenceJobNewDataPrepare(ctx *context.Context) error { } func inferenceJobErrorNewDataPrepare(ctx *context.Context, form auth.CreateModelArtsInferenceJobForm) error { - var taskJobNameKey = ctx.Query("taskJobNameKey") - redis_client.Del(taskJobNameKey) - ctx.Data["PageIsCloudBrain"] = true t := time.Now() From 2eeb3d53b6173622143124c2e85c24a615f7f63d Mon Sep 17 00:00:00 2001 From: zouap Date: Mon, 5 Sep 2022 10:47:35 +0800 Subject: [PATCH 093/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E8=80=81=E6=8B=89=E6=96=B0=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: zouap --- models/user_invitation.go | 6 +++--- modules/grampus/resty.go | 11 ++++++----- routers/api/v1/api.go | 1 + routers/repo/cloudbrain.go | 2 +- routers/repo/grampus.go | 30 ++++++++++++++++++++++++++++++ routers/routes/routes.go | 1 + routers/user/Invitation.go | 13 ++++++++++++- 7 files changed, 54 insertions(+), 10 deletions(-) diff --git a/models/user_invitation.go b/models/user_invitation.go index 816cacdaf..56de43d01 100644 --- a/models/user_invitation.go +++ b/models/user_invitation.go @@ -49,7 +49,7 @@ func InsertInvitaion(invitationUser *Invitation) error { return err } -func QueryInvitaionBySrcUserId(srcUserId int64) ([]*Invitation, int) { +func QueryInvitaionBySrcUserId(srcUserId int64) []*Invitation { statictisSess := xStatistic.NewSession() defer statictisSess.Close() cond := "src_user_id =" + fmt.Sprint(srcUserId) @@ -58,7 +58,7 @@ func QueryInvitaionBySrcUserId(srcUserId int64) ([]*Invitation, int) { if err := statictisSess.Table(new(Invitation)).Where(cond).OrderBy("created_unix desc"). Find(&invitationList); err != nil { - return nil, 0 + return nil } - return invitationList, len(invitationList) + return invitationList } diff --git a/modules/grampus/resty.go b/modules/grampus/resty.go index 5e8722b4b..593abccbb 100755 --- a/modules/grampus/resty.go +++ b/modules/grampus/resty.go @@ -1,14 +1,15 @@ package grampus import ( - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "crypto/tls" "encoding/json" "fmt" - "github.com/go-resty/resty/v2" "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "github.com/go-resty/resty/v2" ) var ( @@ -235,7 +236,7 @@ func GetTrainJobLog(jobID string) (string, error) { return logContent, fmt.Errorf("json.Unmarshal failed(%s): %v", res.String(), err.Error()) } log.Error("GetTrainJobLog failed(%d):%s(%s)", res.StatusCode(), temp.ErrorCode, temp.ErrorMsg) - return logContent, fmt.Errorf("GetTrainJobLog failed(%d):%s(%s)", res.StatusCode(), temp.ErrorCode, temp.ErrorMsg) + return logContent, fmt.Errorf("GetTrainJobLog failed(%d):%d(%s)", res.StatusCode(), temp.ErrorCode, temp.ErrorMsg) } logContent = res.String() diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0b941b400..3e588d942 100755 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -969,6 +969,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("", repo.GetModelArtsTrainJobVersion) m.Post("/stop_version", cloudbrain.AdminOrOwnerOrJobCreaterRightForTrain, repo_ext.GrampusStopJob) m.Get("/log", repo_ext.GrampusGetLog) + m.Get("/download_log", repo_ext.GrampusDownloadLog) }) }) }, reqRepoReader(models.UnitTypeCloudBrain)) diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index c1e89dde5..457f275ed 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -2718,7 +2718,7 @@ func getTrainJobCommand(form auth.CreateCloudBrainForm) (string, error) { } } - command += "python /code/" + bootFile + param + " | tee " + cloudbrain.ModelMountPath + "/" + form.DisplayJobName + "-" + cloudbrain.LogFile + command += "python /code/" + bootFile + param + " > " + cloudbrain.ModelMountPath + "/" + form.DisplayJobName + "-" + cloudbrain.LogFile return command, nil } diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index 33e111df2..4b9ef621c 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -725,6 +725,36 @@ func GrampusTrainJobShow(ctx *context.Context) { ctx.HTML(http.StatusOK, tplGrampusTrainJobShow) } +func GrampusDownloadLog(ctx *context.Context) { + jobID := ctx.Params(":jobid") + job, err := models.GetCloudbrainByJobID(jobID) + if err != nil { + log.Error("GetCloudbrainByJobID failed: %v", err, ctx.Data["MsgID"]) + ctx.ServerError(err.Error(), err) + return + } + + content, err := grampus.GetTrainJobLog(job.JobID) + if err != nil { + log.Error("GetTrainJobLog failed: %v", err, ctx.Data["MsgID"]) + ctx.ServerError(err.Error(), err) + return + } + fileName := job.JobName + "-log.txt" + ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+fileName) + ctx.Resp.Header().Set("Content-Type", "application/octet-stream") + var b []byte = []byte(content) + + ctx.Resp.Write(b) + + // ctx.JSON(http.StatusOK, map[string]interface{}{ + // "JobName": job.JobName, + // "Content": content, + // }) + + //return +} + func GrampusGetLog(ctx *context.Context) { jobID := ctx.Params(":jobid") job, err := models.GetCloudbrainByJobID(jobID) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 3f927ea79..bfa0552ac 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -506,6 +506,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/forgot_password", user.ForgotPasswdPost) m.Post("/logout", user.SignOut) m.Get("/invitation_code", user.GetInvitaionCode) + m.Get("/invitation_tpl", user.InviationTpl) }) // ***** END: User ***** diff --git a/routers/user/Invitation.go b/routers/user/Invitation.go index fc0a03f45..78718b33f 100644 --- a/routers/user/Invitation.go +++ b/routers/user/Invitation.go @@ -6,12 +6,17 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/repository" ) +const ( + tplInvitation base.TplName = "user/settings/invite" +) + func GetInvitaionCode(ctx *context.Context) { url := setting.RecommentRepoAddr + "invitaion_page" @@ -29,11 +34,17 @@ func GetInvitaionCode(ctx *context.Context) { } if ctx.IsSigned { - resultJsonMap["invitaion_code"] = getInvitaionCode(ctx) + resultJsonMap["invitation_code"] = getInvitaionCode(ctx) + resultJsonMap["invitation_users"] = models.QueryInvitaionBySrcUserId(ctx.User.ID) } + ctx.JSON(200, resultJsonMap) } +func InviationTpl(ctx *context.Context) { + ctx.HTML(200, tplInvitation) +} + func RegisteUserByInvitaionCode(form auth.RegisterForm, newUserId int64) error { invitationcode := form.InvitaionCode From a84152653a4bfe421d7bd4da3c786129c9267a54 Mon Sep 17 00:00:00 2001 From: zouap Date: Mon, 5 Sep 2022 11:37:08 +0800 Subject: [PATCH 094/283] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: zouap --- routers/routes/routes.go | 7 +++---- routers/user/auth.go | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 142fde739..af139b7a9 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -375,8 +375,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/login/cloud_brain", user.SignInCloudBrain) m.Post("/login/cloud_brain", bindIgnErr(auth.SignInForm{}), user.SignInCloudBrainPost) m.Post("/login", bindIgnErr(auth.SignInForm{}), user.SignInPost) - - m.Get("/invitaion", user.GetInvitaionCode) + m.Get("/invitation_code", user.GetInvitaionCode) + m.Get("/invitation_tpl", user.InviationTpl) m.Get("/login/phone", user.SignInPhone) m.Post("/login/phone", bindIgnErr(auth.PhoneNumberCodeForm{}), user.SignInPhonePost) m.Group("", func() { @@ -505,8 +505,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/forgot_password", user.ForgotPasswd) m.Post("/forgot_password", user.ForgotPasswdPost) m.Post("/logout", user.SignOut) - m.Get("/invitation_code", user.GetInvitaionCode) - m.Get("/invitation_tpl", user.InviationTpl) + }) // ***** END: User ***** diff --git a/routers/user/auth.go b/routers/user/auth.go index a95d0e3c4..3c80af4e1 100755 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -1337,9 +1337,6 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo ctx.RenderWithErr(ctx.Tr("sign_up_agree_tips"), tplSignUp, &form) return } - if form.InvitaionCode != "" { - RegisteUserByInvitaionCode(ctx, form) - } u := &models.User{ Name: form.UserName, @@ -1369,6 +1366,10 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo } log.Trace("Account created: %s", u.Name, ctx.Data["MsgID"]) + if form.InvitaionCode != "" { + RegisteUserByInvitaionCode(form, u.ID) + } + err := models.AddEmailAddress(&models.EmailAddress{ UID: u.ID, Email: form.Email, From 608859e5ce1d066eb6f9107ce52f304120c3285a Mon Sep 17 00:00:00 2001 From: chenshihai Date: Mon, 5 Sep 2022 14:08:38 +0800 Subject: [PATCH 095/283] Invite Friends --- options/locale/locale_en-US.ini | 1 + options/locale/locale_zh-CN.ini | 1 + public/img/ad/ad01.png | Bin 0 -> 13638 bytes public/img/ad/ad02.png | Bin 0 -> 32334 bytes public/img/ad/ad03.jpg | Bin 0 -> 36620 bytes templates/base/head_navbar.tmpl | 4 + templates/base/head_navbar_fluid.tmpl | 4 + templates/base/head_navbar_home.tmpl | 4 + templates/base/head_navbar_pro.tmpl | 4 + templates/user/auth/signup_inner.tmpl | 13 + templates/user/dashboard/repolist.tmpl | 3 + templates/user/profile.tmpl | 6 + templates/user/settings/invite.tmpl | 7 + web_src/js/features/ad.js | 87 ++++++ web_src/js/index.js | 1 + web_src/js/standalone/phoneverify.js | 20 +- web_src/less/standalone/_phoneverify.less | 3 +- web_src/vuepages/apis/modules/userinvite.js | 11 + web_src/vuepages/pages/user/invite/index.vue | 279 ++++++++++++++++++ .../pages/user/invite/vp-user-invite.js | 17 ++ 20 files changed, 459 insertions(+), 6 deletions(-) create mode 100644 public/img/ad/ad01.png create mode 100644 public/img/ad/ad02.png create mode 100644 public/img/ad/ad03.jpg create mode 100644 templates/user/settings/invite.tmpl create mode 100644 web_src/js/features/ad.js create mode 100644 web_src/vuepages/apis/modules/userinvite.js create mode 100644 web_src/vuepages/pages/user/invite/index.vue create mode 100644 web_src/vuepages/pages/user/invite/vp-user-invite.js diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3453344f7..30c3c3b5b 100755 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -69,6 +69,7 @@ your_dashboard = Dashboard your_profile = Profile your_starred = Starred your_settings = Settings +invite_friends = Invite Friends all = All sources = Sources diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index d527218d3..e1354bdfd 100755 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -69,6 +69,7 @@ your_dashboard=个人中心 your_profile=个人信息 your_starred=已点赞 your_settings=设置 +invite_friends=邀请好友 all=所有 sources=自建 diff --git a/public/img/ad/ad01.png b/public/img/ad/ad01.png new file mode 100644 index 0000000000000000000000000000000000000000..379c39bd155af3eb2ff359338f78a1713844ea34 GIT binary patch literal 13638 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 {{.i18n.Tr "custom.Platform_Tutorial"}} + + + {{.i18n.Tr "invite_friends"}} + {{if .IsAdmin}}
diff --git a/templates/base/head_navbar_fluid.tmpl b/templates/base/head_navbar_fluid.tmpl index 84781db11..9e31cc2db 100644 --- a/templates/base/head_navbar_fluid.tmpl +++ b/templates/base/head_navbar_fluid.tmpl @@ -177,6 +177,10 @@ {{.i18n.Tr "custom.Platform_Tutorial"}} + + + + {{.i18n.Tr "invite_friends"}} {{if .IsAdmin}}
diff --git a/templates/base/head_navbar_home.tmpl b/templates/base/head_navbar_home.tmpl index 64e04b4c3..471540f64 100644 --- a/templates/base/head_navbar_home.tmpl +++ b/templates/base/head_navbar_home.tmpl @@ -160,6 +160,10 @@ {{.i18n.Tr "custom.Platform_Tutorial"}} + + + {{.i18n.Tr "invite_friends"}} + {{if .IsAdmin}}
diff --git a/templates/base/head_navbar_pro.tmpl b/templates/base/head_navbar_pro.tmpl index e9f662bbe..0b5babf6e 100644 --- a/templates/base/head_navbar_pro.tmpl +++ b/templates/base/head_navbar_pro.tmpl @@ -181,6 +181,10 @@ {{.i18n.Tr "custom.Platform_Tutorial"}} + + + {{.i18n.Tr "invite_friends"}} + {{if .IsAdmin}}
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index 6ba10e8e7..43667f6f5 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -35,6 +35,9 @@ {{if .DisableRegistration}}

{{.i18n.Tr "auth.disable_register_prompt"}}

{{else}} +
+ 您的好友 Itx003 邀请你加入启智社区AI协作平台,畅享充沛的免费算力资源! +
@@ -71,6 +74,16 @@ {{template "user/auth/phone_verify" .}}
{{end}} + +
+
+
+ 推荐人 +
+ +
+
+
diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index ff85d72d4..b89fdbf1f 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -18,6 +18,9 @@ v-cloak >
+
+ +
@@ -107,3 +107,29 @@
+ diff --git a/web_src/vuepages/pages/user/invite/index.vue b/web_src/vuepages/pages/user/invite/index.vue index 86cdc7a54..b49db76cb 100644 --- a/web_src/vuepages/pages/user/invite/index.vue +++ b/web_src/vuepages/pages/user/invite/index.vue @@ -6,25 +6,25 @@
- -
邀请好友来启智,用免费算力还能赚奖金!
+ +
{{ bannerTitle }}
- 新一期的开源打榜活动,每邀请一名好友注册并激活,就可以获得5打榜积分。快快邀请更多好友帮你冲击榜单吧~ - 点击查看活动详情 + {{ pageLinkDesc }} + 点击查看活动详情
- 启智AI协作平台是启智社区面向AI开发者提供的一站式AI开发协作平台,提供了代码托管、数据集管理、基于异构计算资源的模型调试与训练等功能。目前已经与鹏城云脑、中国算力网(C²NET)一期打通,免费提供丰富算力资源,支撑大家完成AI开发任务。 + {{ pageOpeniDesc }}
-
{{ sharedLink }}
-
推荐人:{{ sharedUser }}
+
{{ invitationLink + invitationCode }}
+
推荐人:{{ invitationCode }}
复制注册邀请链接
- +
@@ -43,9 +43,7 @@ {{ scope.row.statusStr }} - - - + From 247dc9051d5f8d5e2e3e741b12f150eb860094a4 Mon Sep 17 00:00:00 2001 From: zhoupzh Date: Thu, 8 Sep 2022 11:29:04 +0800 Subject: [PATCH 124/283] fix issue --- web_src/js/features/cloudbrainShow.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/cloudbrainShow.js b/web_src/js/features/cloudbrainShow.js index 73a8ed7b6..229672f20 100644 --- a/web_src/js/features/cloudbrainShow.js +++ b/web_src/js/features/cloudbrainShow.js @@ -130,11 +130,14 @@ export default async function initCloudrainSow() { let repoPath = $(`#accordion${version_name}`).data("repopath"); $(`#log_file${version_name}`).siblings("pre").remove(); let end_line = $(`#log${version_name} input[name=end_line]`).val(); - $(".ui.inverted.active.dimmer").css("display", "block"); + $(`#log${version_name} .ui.inverted.active.dimmer`).css("display", "block"); $.get( `/api/v1/repos/${repoPath}/${ID}/log?version_name=${version_name}&base_line=&lines=50&order=desc`, (data) => { - $(".ui.inverted.active.dimmer").css("display", "none"); + $(`#log${version_name} .ui.inverted.active.dimmer`).css( + "display", + "none" + ); if (!data.CanLogDownload) { $(`#${version_name}-log-down`) .removeClass("ti-download-file") From 83261b23c00a2b6151edf5d650b4d80e8d0b9b5c Mon Sep 17 00:00:00 2001 From: chenshihai Date: Thu, 8 Sep 2022 14:45:27 +0800 Subject: [PATCH 125/283] invitation advertisement --- public/img/ad/ad01.png | Bin 13638 -> 0 bytes public/img/ad/ad02.png | Bin 32334 -> 0 bytes public/img/ad/ad03.jpg | Bin 36620 -> 0 bytes templates/user/dashboard/repolist.tmpl | 4 +- templates/user/profile.tmpl | 28 ++++++- web_src/js/features/ad.js | 107 ++++++++++++++++++------- 6 files changed, 109 insertions(+), 30 deletions(-) delete mode 100644 public/img/ad/ad01.png delete mode 100644 public/img/ad/ad02.png delete mode 100644 public/img/ad/ad03.jpg diff --git a/public/img/ad/ad01.png b/public/img/ad/ad01.png deleted file mode 100644 index 379c39bd155af3eb2ff359338f78a1713844ea34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13638 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
+ {{if .invite_image_url}}
- +
+ {{end}} `); + adEl.data('data', adI); $('body').append(adEl); } window.localStorage.setItem('ads', JSON.stringify(adInfoObj)); @@ -73,15 +102,39 @@ var offSet = scrollTop - scrollTopOld; scrollTopOld = scrollTop; timeHandler && clearTimeout(timeHandler); - $('.__ad_c__').animate({ bottom: 50 + offSet + 'px' }, 0); + $('.__ad_c__').each(function (_, item) { + var self = $(item); + var adData = self.data('data'); + if (adData.pos.bottom !== undefined) { + self.animate({ bottom: adData.pos.bottom + offSet + 'px' }, 0); + } + if (adData.pos.top !== undefined) { + self.animate({ top: adData.pos.top - offSet + 'px' }, 0); + } + }) timeHandler = setTimeout(function () { - $('.__ad_c__').animate({ bottom: 50 + 'px' }, 0); + $('.__ad_c__').each(function (_, item) { + var self = $(item); + var adData = self.data('data'); + if (adData.pos.bottom !== undefined) { + self.animate({ bottom: adData.pos.bottom + 'px' }, 0); + } + if (adData.pos.top !== undefined) { + self.animate({ top: adData.pos.top + 'px' }, 0); + } + }) }, 20); }); } setTimeout(function () { - createAd(adList); + if (!$('meta[name="_uid"]').length) { // 未登录,不显示 + window.localStorage.removeItem('ads'); + return; + } + var pathName = window.location.pathname; + if (exceptPages.indexOf(pathName) > -1) return; // 排除页,不显示 + initAd(); initAdEvent(); }, 0); })(); From a1cb52cef2b892bb3e148e9bd8cc55e817604835 Mon Sep 17 00:00:00 2001 From: liuzx Date: Thu, 8 Sep 2022 17:58:57 +0800 Subject: [PATCH 126/283] grampus version new --- routers/repo/grampus.go | 142 +++++++++++++++++- routers/routes/routes.go | 2 + .../repo/grampus/trainjob/gpu/versionnew.tmpl | 0 .../repo/grampus/trainjob/npu/versionnew.tmpl | 0 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 templates/repo/grampus/trainjob/gpu/versionnew.tmpl create mode 100644 templates/repo/grampus/trainjob/npu/versionnew.tmpl diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index d7e799427..f4ab40f5e 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -1,7 +1,6 @@ package repo import ( - "code.gitea.io/gitea/services/cloudbrain/resource" "encoding/json" "errors" "fmt" @@ -13,6 +12,8 @@ import ( "strings" "time" + "code.gitea.io/gitea/services/cloudbrain/resource" + "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/grampus" @@ -34,10 +35,12 @@ const ( tplGrampusTrainJobShow base.TplName = "repo/grampus/trainjob/show" //GPU - tplGrampusTrainJobGPUNew base.TplName = "repo/grampus/trainjob/gpu/new" + tplGrampusTrainJobGPUNew base.TplName = "repo/grampus/trainjob/gpu/new" + tplGrampusTrainJobGPUVersionNew base.TplName = "repo/grampus/trainjob/gpu/versionnew" //NPU - tplGrampusTrainJobNPUNew base.TplName = "repo/grampus/trainjob/npu/new" + tplGrampusTrainJobNPUNew base.TplName = "repo/grampus/trainjob/npu/new" + tplGrampusTrainJobNPUVersionNew base.TplName = "repo/grampus/trainjob/npu/versionnew" ) func GrampusTrainJobGPUNew(ctx *context.Context) { @@ -137,6 +140,126 @@ func grampusTrainJobNewDataPrepare(ctx *context.Context, processType string) err return nil } +func GrampusTrainJobVersionNew(ctx *context.Context) { + task := ctx.Cloudbrain + if task.ComputeResource == models.GPUResource { + err := grampusTrainJobVersionNewDataPrepare(ctx, grampus.ProcessorTypeGPU) + if err != nil { + ctx.ServerError("get new train-job version info failed", err) + return + } + ctx.HTML(http.StatusOK, tplGrampusTrainJobGPUVersionNew) + } else if task.ComputeResource == models.NPUResource { + err := grampusTrainJobVersionNewDataPrepare(ctx, grampus.ProcessorTypeNPU) + if err != nil { + ctx.ServerError("get new train-job version info failed", err) + return + } + ctx.HTML(200, tplGrampusTrainJobNPUVersionNew) + } +} + +func grampusTrainJobVersionNewDataPrepare(ctx *context.Context, processType string) error { + ctx.Data["PageIsCloudBrain"] = true + + t := time.Now() + var displayJobName = jobNamePrefixValid(cutString(ctx.User.Name, 5)) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[5:] + ctx.Data["display_job_name"] = displayJobName + + //get valid images + images, err := grampus.GetImages(processType) + if err != nil { + log.Error("GetImages failed:", err.Error()) + } else { + ctx.Data["images"] = images.Infos + } + + grampus.InitSpecialPool() + + ctx.Data["GPUEnabled"] = true + ctx.Data["NPUEnabled"] = true + includeCenters := make(map[string]struct{}) + excludeCenters := make(map[string]struct{}) + if grampus.SpecialPools != nil { + for _, pool := range grampus.SpecialPools.Pools { + if pool.IsExclusive { + if !IsUserInOrgPool(ctx.User.ID, pool) { + ctx.Data[pool.Type+"Enabled"] = false + } + } else { + if strings.Contains(strings.ToLower(processType), strings.ToLower(pool.Type)) { + if IsUserInOrgPool(ctx.User.ID, pool) { + for _, center := range pool.Pool { + includeCenters[center.Queue] = struct{}{} + } + } else { + for _, center := range pool.Pool { + excludeCenters[center.Queue] = struct{}{} + } + + } + + } + + } + } + } + + //prepare available specs + if processType == grampus.ProcessorTypeNPU { + prepareGrampusTrainSpecs(ctx, models.NPU) + } else if processType == grampus.ProcessorTypeGPU { + prepareGrampusTrainSpecs(ctx, models.GPU) + } + + //get branches + branches, _, err := ctx.Repo.GitRepo.GetBranches(0, 0) + if err != nil { + log.Error("GetBranches error:", err.Error()) + } else { + ctx.Data["branches"] = branches + } + + ctx.Data["BranchName"] = ctx.Cloudbrain.BranchName + ctx.Data["ImageName"] = ctx.Cloudbrain.Image + ctx.Data["BootFile"] = ctx.Cloudbrain.BootFile + ctx.Data["description"] = ctx.Cloudbrain.Description + spec, _ := resource.GetCloudbrainSpec(ctx.Cloudbrain.ID) + if spec != nil { + log.Info("spec_id = %d", spec.ID) + ctx.Data["spec_id"] = spec.ID + } + var Parameters modelarts.Parameters + if err = json.Unmarshal([]byte(ctx.Cloudbrain.Parameters), &Parameters); err != nil { + ctx.ServerError("json.Unmarshal failed:", err) + return err + } + ctx.Data["params"] = Parameters.Parameter + + _, _, datasetNames, _, err := getDatasUrlListByUUIDS(ctx.Cloudbrain.Uuid) + if err != nil { + log.Info("query dataset error," + err.Error()) + ctx.Data["dataset_name"] = "" + } else { + ctx.Data["dataset_name"] = datasetNames + } + ctx.Data["uuid"] = ctx.Cloudbrain.Uuid + ctx.Data["ComputeResource"] = ctx.Cloudbrain.ComputeResource + + if processType == grampus.ProcessorTypeGPU { + ctx.Data["datasetType"] = models.TypeCloudBrainOne + waitCount := cloudbrain.GetWaitingCloudbrainCount(models.TypeC2Net, models.GPUResource, models.JobTypeTrain) + ctx.Data["WaitCount"] = waitCount + } else if processType == grampus.ProcessorTypeNPU { + ctx.Data["datasetType"] = models.TypeCloudBrainTwo + waitCount := cloudbrain.GetWaitingCloudbrainCount(models.TypeC2Net, models.NPUResource, models.JobTypeTrain) + ctx.Data["WaitCount"] = waitCount + ctx.Data["work_server_number"] = ctx.Cloudbrain.WorkServerNumber + } + + return nil +} + func prepareGrampusTrainSpecs(ctx *context.Context, computeResource string) { noteBookSpecs, _ := resource.FindAvailableSpecs(ctx.User.ID, models.FindSpecsOptions{ JobType: models.JobTypeTrain, @@ -378,6 +501,19 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/modelarts/train-job") } +func GrampusTrainJobVersionCreate(ctx *context.Context, form auth.CreateGrampusTrainJobForm) { + computeResource := ctx.Query("compute_resource") + if computeResource == models.GPUResource { + GrampusTrainJobGpuCreate(ctx, form) + } else if computeResource == models.NPUResource { + GrampusTrainJobNpuCreate(ctx, form) + } else { + ctx.ServerError("resource error", errors.New("compute resource is not support")) + return + } + +} + func checkSpecialPool(ctx *context.Context, resourceType string) string { grampus.InitSpecialPool() if grampus.SpecialPools != nil { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index d8b1bff22..82105ffd1 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -1159,6 +1159,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/stop", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.GrampusStopJob) m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRightForTrain, repo.GrampusTrainJobDel) m.Get("/model_download", cloudbrain.AdminOrJobCreaterRightForTrain, repo.ModelDownload) + m.Get("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, repo.GrampusTrainJobVersionNew) + m.Post("/create_version", reqWechatBind, cloudbrain.AdminOrOwnerOrJobCreaterRightForTrain, bindIgnErr(auth.CreateGrampusTrainJobForm{}), repo.GrampusTrainJobVersionCreate) }) m.Group("/gpu", func() { m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.GrampusTrainJobGPUNew) diff --git a/templates/repo/grampus/trainjob/gpu/versionnew.tmpl b/templates/repo/grampus/trainjob/gpu/versionnew.tmpl new file mode 100644 index 000000000..e69de29bb diff --git a/templates/repo/grampus/trainjob/npu/versionnew.tmpl b/templates/repo/grampus/trainjob/npu/versionnew.tmpl new file mode 100644 index 000000000..e69de29bb From 31959dea0b848866e2708a9c380cc184d8cceb72 Mon Sep 17 00:00:00 2001 From: liuzx Date: Tue, 13 Sep 2022 11:10:44 +0800 Subject: [PATCH 127/283] fix-2817 --- routers/repo/cloudbrain.go | 81 +++++++++++++++++++++++++++++++++++++- routers/routes/routes.go | 4 +- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 7020f0a61..ec3bf1f39 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -2,7 +2,6 @@ package repo import ( "bufio" - "code.gitea.io/gitea/services/cloudbrain/resource" "encoding/json" "errors" "fmt" @@ -16,6 +15,8 @@ import ( "time" "unicode/utf8" + "code.gitea.io/gitea/services/cloudbrain/resource" + "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/grampus" @@ -140,6 +141,71 @@ func cloudBrainNewDataPrepare(ctx *context.Context) error { return nil } +func cloudBrainVersionNewDataPrepare(ctx *context.Context) error { + ctx.Data["PageIsCloudBrain"] = true + t := time.Now() + var displayJobName = jobNamePrefixValid(cutString(ctx.User.Name, 5)) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[5:] + ctx.Data["display_job_name"] = displayJobName + + ctx.Data["command"] = cloudbrain.GetCloudbrainDebugCommand() + ctx.Data["code_path"] = cloudbrain.CodeMountPath + ctx.Data["dataset_path"] = cloudbrain.DataSetMountPath + ctx.Data["model_path"] = cloudbrain.ModelMountPath + ctx.Data["benchmark_path"] = cloudbrain.BenchMarkMountPath + ctx.Data["is_benchmark_enabled"] = setting.IsBenchmarkEnabled + + if categories == nil { + json.Unmarshal([]byte(setting.BenchmarkCategory), &categories) + } + ctx.Data["benchmark_categories"] = categories.Category + + ctx.Data["benchmark_types"] = GetBenchmarkTypes(ctx).BenchmarkType + queuesDetail, _ := cloudbrain.GetQueuesDetail() + if queuesDetail != nil { + ctx.Data["QueuesDetail"] = queuesDetail + } + + prepareCloudbrainOneSpecs(ctx) + + ctx.Data["snn4imagenet_path"] = cloudbrain.Snn4imagenetMountPath + ctx.Data["is_snn4imagenet_enabled"] = setting.IsSnn4imagenetEnabled + ctx.Data["brainscore_path"] = cloudbrain.BrainScoreMountPath + ctx.Data["is_brainscore_enabled"] = setting.IsBrainScoreEnabled + ctx.Data["benchmarkMode"] = ctx.Query("benchmarkMode") + + ctx.Data["branch_name"] = ctx.Cloudbrain.BranchName + ctx.Data["image_name"] = ctx.Cloudbrain.Image + ctx.Data["bootfile"] = ctx.Cloudbrain.BootFile + ctx.Data["description"] = ctx.Cloudbrain.Description + spec, _ := resource.GetCloudbrainSpec(ctx.Cloudbrain.ID) + if spec != nil { + log.Info("spec_id = %d", spec.ID) + ctx.Data["spec_id"] = spec.ID + } + var Parameters modelarts.Parameters + if err := json.Unmarshal([]byte(ctx.Cloudbrain.Parameters), &Parameters); err != nil { + ctx.ServerError("json.Unmarshal failed:", err) + return err + } + ctx.Data["params"] = Parameters.Parameter + + _, _, datasetNames, _, err := getDatasUrlListByUUIDS(ctx.Cloudbrain.Uuid) + if err != nil { + log.Info("query dataset error," + err.Error()) + ctx.Data["dataset_name"] = "" + } else { + ctx.Data["dataset_name"] = datasetNames + } + ctx.Data["uuid"] = ctx.Cloudbrain.Uuid + ctx.Data["compute_resource"] = ctx.Cloudbrain.ComputeResource + + ctx.Data["dataset_type"] = models.TypeCloudBrainOne + waitCount := cloudbrain.GetWaitingCloudbrainCount(models.TypeC2Net, models.GPUResource, models.JobTypeTrain) + ctx.Data["wait_count"] = waitCount + + return nil +} + func prepareCloudbrainOneSpecs(ctx *context.Context) { debugSpecs, _ := resource.FindAvailableSpecs(ctx.User.ID, models.FindSpecsOptions{ JobType: models.JobTypeDebug, @@ -342,6 +408,10 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { } } +func CloudBrainTrainJobVersionCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { + CloudBrainCreate(ctx, form) +} + func loadCodeAndMakeModelPath(repo *models.Repository, codePath string, branchName string, jobName string, resultPath string) string { err := downloadCode(repo, codePath, branchName) if err != nil { @@ -2548,6 +2618,15 @@ func CloudBrainTrainJobNew(ctx *context.Context) { ctx.HTML(http.StatusOK, tplCloudBrainTrainJobNew) } +func CloudBrainTrainJobVersionNew(ctx *context.Context) { + err := cloudBrainVersionNewDataPrepare(ctx) + if err != nil { + ctx.ServerError("get new train-job info failed", err) + return + } + ctx.HTML(http.StatusOK, tplCloudBrainTrainJobNew) +} + func InferenceCloudBrainJobNew(ctx *context.Context) { err := cloudBrainNewDataPrepare(ctx) if err != nil { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 82105ffd1..e97d4a0ca 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -1137,6 +1137,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/download_model", cloudbrain.AdminOrOwnerOrJobCreaterRightForTrain, repo.CloudBrainDownloadModel) //m.Get("/get_log", cloudbrain.AdminOrJobCreaterRightForTrain, repo.GetLogFromModelDir) //m.Post("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreateVersion) + m.Get("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, repo.CloudBrainTrainJobVersionNew) + m.Post("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, bindIgnErr(auth.CreateGrampusTrainJobForm{}), repo.CloudBrainTrainJobVersionCreate) }) m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.CloudBrainTrainJobNew) m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) @@ -1160,7 +1162,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRightForTrain, repo.GrampusTrainJobDel) m.Get("/model_download", cloudbrain.AdminOrJobCreaterRightForTrain, repo.ModelDownload) m.Get("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, repo.GrampusTrainJobVersionNew) - m.Post("/create_version", reqWechatBind, cloudbrain.AdminOrOwnerOrJobCreaterRightForTrain, bindIgnErr(auth.CreateGrampusTrainJobForm{}), repo.GrampusTrainJobVersionCreate) + m.Post("/create_version", reqWechatBind, cloudbrain.AdminOrJobCreaterRightForTrain, bindIgnErr(auth.CreateGrampusTrainJobForm{}), repo.GrampusTrainJobVersionCreate) }) m.Group("/gpu", func() { m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.GrampusTrainJobGPUNew) From b226fe7b5c6040a15c2fdc0af2aa01f5b6c020c1 Mon Sep 17 00:00:00 2001 From: openihu Date: Tue, 13 Sep 2022 11:22:37 +0800 Subject: [PATCH 128/283] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A1=B5=E8=84=9A?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=8F=8A=E8=A1=8C=E9=97=B4=E8=B7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/base/footer_content.tmpl | 10 +++++----- templates/base/footer_content_fluid.tmpl | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) mode change 100644 => 100755 templates/base/footer_content_fluid.tmpl diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl index bcf46f9f0..aec7b8d61 100755 --- a/templates/base/footer_content.tmpl +++ b/templates/base/footer_content.tmpl @@ -20,7 +20,7 @@ {{.i18n.Tr "custom.Platform_Tutorial"}} - {{if .EnableSwagger}} API{{end}} + {{if .EnableSwagger}} API{{end}} {{if .IsSigned}} {{.i18n.Tr "custom.foot.advice_feedback"}} {{else}} - {{.i18n.Tr "custom.foot.advice_feedback"}} + {{.i18n.Tr "custom.foot.advice_feedback"}} {{end}} {{template "custom/extra_links_footer" .}} diff --git a/templates/base/footer_content_fluid.tmpl b/templates/base/footer_content_fluid.tmpl index 29395a045..24b18e94d 100755 --- a/templates/base/footer_content_fluid.tmpl +++ b/templates/base/footer_content_fluid.tmpl @@ -26,12 +26,12 @@ {{end}}
- {{.i18n.Tr "custom.Platform_Tutorial"}} + {{.i18n.Tr "custom.Platform_Tutorial"}} {{if .EnableSwagger}} API{{end}} {{if .IsSigned}} {{.i18n.Tr "custom.foot.advice_feedback"}} {{else}} - {{.i18n.Tr "custom.foot.advice_feedback"}} + {{.i18n.Tr "custom.foot.advice_feedback"}} {{end}} {{template "custom/extra_links_footer" .}}
From e54de5eb15243ad2ff26a65c4e49ffcdb3ea48e4 Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Tue, 13 Sep 2022 11:39:28 +0800 Subject: [PATCH 131/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/cloudbrain.go | 71 +++++++++----- modules/auth/cloudbrain.go | 5 + modules/auth/grampus.go | 5 + modules/auth/modelarts.go | 5 + modules/cloudbrain/cloudbrain.go | 14 +++ modules/grampus/grampus.go | 73 +++++++++------ routers/repo/cloudbrain.go | 13 +++ routers/repo/grampus.go | 155 ++++++++++++++++++++++++------- 8 files changed, 254 insertions(+), 87 deletions(-) diff --git a/models/cloudbrain.go b/models/cloudbrain.go index dc56efef7..12c76ce57 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "path" "strconv" "strings" "time" @@ -170,24 +171,25 @@ type Cloudbrain struct { ImageID string //grampus image_id AiCenter string //grampus ai center: center_id+center_name - TrainUrl string //输出模型的obs路径 - BranchName string //分支名称 - Parameters string //传给modelarts的param参数 - BootFile string //启动文件 - DataUrl string //数据集的obs路径 - LogUrl string //日志输出的obs路径 - PreVersionId int64 //父版本的版本id - FlavorCode string //modelarts上的规格id - Description string `xorm:"varchar(256)"` //描述 - WorkServerNumber int //节点数 - FlavorName string //规格名称 - EngineName string //引擎名称 - TotalVersionCount int //任务的所有版本数量,包括删除的 - LabelName string //标签名称 - ModelName string //模型名称 - ModelVersion string //模型版本 - CkptName string //权重文件名称 - ResultUrl string //推理结果的obs路径 + TrainUrl string //输出模型的obs路径 + BranchName string //分支名称 + Parameters string //传给modelarts的param参数 + BootFile string //启动文件 + DataUrl string //数据集的obs路径 + LogUrl string //日志输出的obs路径 + PreVersionId int64 //父版本的版本id + FlavorCode string //modelarts上的规格id + Description string `xorm:"varchar(256)"` //描述 + WorkServerNumber int //节点数 + FlavorName string //规格名称 + EngineName string //引擎名称 + TotalVersionCount int //任务的所有版本数量,包括删除的 + LabelName string //标签名称 + ModelName string //模型名称 + ModelVersion string //模型版本 + CkptName string //权重文件名称 + PreTrainingModelUrl string //预训练模型地址 + ResultUrl string //推理结果的obs路径 User *User `xorm:"-"` Repo *Repository `xorm:"-"` @@ -603,6 +605,16 @@ type ResourceSpec struct { ShareMemMiB int `json:"shareMemMiB"` } +type FlavorInfos struct { + FlavorInfo []*FlavorInfo `json:"flavor_info"` +} + +type FlavorInfo struct { + Id int `json:"id"` + Value string `json:"value"` + Desc string `json:"desc"` +} + type SpecialPools struct { Pools []*SpecialPool `json:"pools"` } @@ -2223,9 +2235,10 @@ func CloudbrainAllStatic(opts *CloudbrainsOptions) ([]*CloudbrainInfo, int64, er type DatasetInfo struct { DataLocalPath string Name string + FullName string } -func GetDatasetInfo(uuidStr string) (map[string]DatasetInfo, string, error) { +func GetDatasetInfo(uuidStr string, grampusType ...string) (map[string]DatasetInfo, string, error) { var datasetNames string uuids := strings.Split(uuidStr, ";") if len(uuids) > setting.MaxDatasetNum { @@ -2258,16 +2271,26 @@ func GetDatasetInfo(uuidStr string) (map[string]DatasetInfo, string, error) { return nil, datasetNames, errors.New("the dataset name is same") } } + var dataLocalPath string + if len(grampusType) > 0 { + if grampusType[0] == GPU { + dataLocalPath = setting.Attachment.Minio.BasePath + path.Join(attach.UUID[0:1], attach.UUID[1:2]) + "/" + attach.UUID + } else { + dataLocalPath = setting.BasePath + path.Join(attach.UUID[0:1], attach.UUID[1:2]) + "/" + attach.UUID + "/" + } - dataLocalPath := setting.Attachment.Minio.RealPath + - setting.Attachment.Minio.Bucket + "/" + - setting.Attachment.Minio.BasePath + - AttachmentRelativePath(attach.UUID) + - attach.UUID + } else { + dataLocalPath = setting.Attachment.Minio.RealPath + + setting.Attachment.Minio.Bucket + "/" + + setting.Attachment.Minio.BasePath + + AttachmentRelativePath(attach.UUID) + + attach.UUID + } datasetInfos[attach.UUID] = DatasetInfo{ DataLocalPath: dataLocalPath, Name: fileName, + FullName: attach.Name, } if i == 0 { datasetNames = attach.Name diff --git a/modules/auth/cloudbrain.go b/modules/auth/cloudbrain.go index 5bd294f2a..48e23efac 100755 --- a/modules/auth/cloudbrain.go +++ b/modules/auth/cloudbrain.go @@ -23,6 +23,11 @@ type CreateCloudBrainForm struct { BootFile string `form:"boot_file"` Params string `form:"run_para_list"` BranchName string `form:"branch_name"` + ModelName string `form:"model_name"` + ModelVersion string `form:"model_version"` + CkptName string `form:"ckpt_name"` + LabelName string `form:"label_names"` + PreTrainModelUrl string `form:"pre_train_model_url"` DatasetName string `form:"dataset_name"` SpecId int64 `form:"spec_id"` } diff --git a/modules/auth/grampus.go b/modules/auth/grampus.go index 21008ea09..414a7c25d 100755 --- a/modules/auth/grampus.go +++ b/modules/auth/grampus.go @@ -18,6 +18,11 @@ type CreateGrampusTrainJobForm struct { WorkServerNumber int `form:"work_server_number" binding:"Required"` Image string `form:"image"` DatasetName string `form:"dataset_name"` + ModelName string `form:"model_name"` + ModelVersion string `form:"model_version"` + CkptName string `form:"ckpt_name"` + LabelName string `form:"label_names"` + PreTrainModelUrl string `form:"pre_train_model_url"` SpecId int64 `form:"spec_id"` } diff --git a/modules/auth/modelarts.go b/modules/auth/modelarts.go index 23e1f325a..ced5ea1e8 100755 --- a/modules/auth/modelarts.go +++ b/modules/auth/modelarts.go @@ -48,6 +48,11 @@ type CreateModelArtsTrainJobForm struct { FlavorName string `form:"flaver_names" binding:"Required"` EngineName string `form:"engine_names" binding:"Required"` SpecId int64 `form:"spec_id" binding:"Required"` + ModelName string `form:"model_name"` + ModelVersion string `form:"model_version"` + CkptName string `form:"ckpt_name"` + LabelName string `form:"label_names"` + PreTrainModelUrl string `form:"pre_train_model_url"` } type CreateModelArtsInferenceJobForm struct { diff --git a/modules/cloudbrain/cloudbrain.go b/modules/cloudbrain/cloudbrain.go index 748af4a29..5a4d2fe05 100755 --- a/modules/cloudbrain/cloudbrain.go +++ b/modules/cloudbrain/cloudbrain.go @@ -24,6 +24,7 @@ const ( CodeMountPath = "/code" DataSetMountPath = "/dataset" ModelMountPath = "/model" + PretrainModelMountPath = "/pretrainmodel" LogFile = "log.txt" BenchMarkMountPath = "/benchmark" BenchMarkResourceID = 1 @@ -77,6 +78,8 @@ type GenerateCloudBrainTaskReq struct { ModelVersion string CkptName string LabelName string + PreTrainModelPath string + PreTrainingModelUrl string Spec *models.Specification } @@ -276,6 +279,16 @@ func GenerateTask(req GenerateCloudBrainTaskReq) error { }, } + if req.PreTrainingModelUrl != "" { //预训练 + volumes = append(volumes, models.Volume{ + HostPath: models.StHostPath{ + Path: req.PreTrainModelPath, + MountPath: PretrainModelMountPath, + ReadOnly: true, + }, + }) + } + if len(req.DatasetInfos) == 1 { volumes = append(volumes, models.Volume{ HostPath: models.StHostPath{ @@ -359,6 +372,7 @@ func GenerateTask(req GenerateCloudBrainTaskReq) error { CkptName: req.CkptName, ResultUrl: req.ResultPath, LabelName: req.LabelName, + PreTrainingModelUrl: req.PreTrainingModelUrl, CreatedUnix: createTime, UpdatedUnix: createTime, CommitID: req.CommitID, diff --git a/modules/grampus/grampus.go b/modules/grampus/grampus.go index 687fb4959..f434a484c 100755 --- a/modules/grampus/grampus.go +++ b/modules/grampus/grampus.go @@ -62,9 +62,17 @@ type GenerateTrainJobReq struct { TotalVersionCount int ComputeResource string ProcessType string - DatasetName string - Params string - Spec *models.Specification + + DatasetNames string + DatasetInfos map[string]models.DatasetInfo + Params string + ModelName string + LabelName string + CkptName string + ModelVersion string + PreTrainModelPath string + PreTrainingModelUrl string + Spec *models.Specification } func GenerateTrainJob(ctx *context.Context, req *GenerateTrainJobReq) (err error) { @@ -94,33 +102,38 @@ func GenerateTrainJob(ctx *context.Context, req *GenerateTrainJobReq) (err error jobID := jobResult.JobInfo.JobID err = models.CreateCloudbrain(&models.Cloudbrain{ - Status: TransTrainJobStatus(jobResult.JobInfo.Status), - UserID: ctx.User.ID, - RepoID: ctx.Repo.Repository.ID, - JobID: jobID, - JobName: req.JobName, - DisplayJobName: req.DisplayJobName, - JobType: string(models.JobTypeTrain), - Type: models.TypeC2Net, - Uuid: req.Uuid, - DatasetName: req.DatasetName, - CommitID: req.CommitID, - IsLatestVersion: req.IsLatestVersion, - ComputeResource: req.ComputeResource, - ImageID: req.ImageId, - TrainUrl: req.TrainUrl, - BranchName: req.BranchName, - Parameters: req.Params, - BootFile: req.BootFile, - DataUrl: req.DataUrl, - Description: req.Description, - WorkServerNumber: req.WorkServerNumber, - EngineName: req.EngineName, - VersionCount: req.VersionCount, - TotalVersionCount: req.TotalVersionCount, - CreatedUnix: createTime, - UpdatedUnix: createTime, - Spec: req.Spec, + Status: TransTrainJobStatus(jobResult.JobInfo.Status), + UserID: ctx.User.ID, + RepoID: ctx.Repo.Repository.ID, + JobID: jobID, + JobName: req.JobName, + DisplayJobName: req.DisplayJobName, + JobType: string(models.JobTypeTrain), + Type: models.TypeC2Net, + Uuid: req.Uuid, + DatasetName: req.DatasetNames, + CommitID: req.CommitID, + IsLatestVersion: req.IsLatestVersion, + ComputeResource: req.ComputeResource, + ImageID: req.ImageId, + TrainUrl: req.TrainUrl, + BranchName: req.BranchName, + Parameters: req.Params, + BootFile: req.BootFile, + DataUrl: req.DataUrl, + Description: req.Description, + WorkServerNumber: req.WorkServerNumber, + EngineName: req.EngineName, + VersionCount: req.VersionCount, + TotalVersionCount: req.TotalVersionCount, + CreatedUnix: createTime, + UpdatedUnix: createTime, + Spec: req.Spec, + ModelName: req.ModelName, + ModelVersion: req.ModelVersion, + LabelName: req.LabelName, + PreTrainingModelUrl: req.PreTrainingModelUrl, + CkptName: req.CkptName, }) if err != nil { diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 7020f0a61..56a485b66 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -328,6 +328,16 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { Spec: spec, } + if form.ModelName != "" { //使用预训练模型训练 + req.ModelName = form.ModelName + req.LabelName = form.LabelName + req.CkptName = form.CkptName + req.ModelVersion = form.ModelVersion + req.PreTrainModelPath = setting.Attachment.Minio.RealPath + form.PreTrainModelUrl + req.PreTrainingModelUrl = form.PreTrainModelUrl + + } + err = cloudbrain.GenerateTask(req) if err != nil { cloudBrainNewDataPrepare(ctx) @@ -2629,6 +2639,9 @@ func getTrainJobCommand(form auth.CreateCloudBrainForm) (string, error) { param += " --" + parameter.Label + "=" + parameter.Value } } + if form.CkptName != "" { + param += " --pretrainmodelname" + "=" + form.CkptName + } command += "python /code/" + bootFile + param + " > " + cloudbrain.ModelMountPath + "/" + form.DisplayJobName + "-" + cloudbrain.LogFile diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index d7e799427..b32070a84 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -210,7 +210,6 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain repo := ctx.Repo.Repository codeLocalPath := setting.JobPath + jobName + cloudbrain.CodeMountPath + "/" codeMinioPath := setting.CBCodePathPrefix + jobName + cloudbrain.CodeMountPath + "/" - dataMinioPath := setting.Attachment.Minio.BasePath + path.Join(uuid[0:1], uuid[1:2]) + "/" + uuid branchName := form.BranchName image := strings.TrimSpace(form.Image) @@ -290,11 +289,12 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain } //check dataset - attachment, err := models.GetAttachmentByUUID(uuid) + + datasetInfos, datasetNames, err := models.GetDatasetInfo(uuid, models.GPU) if err != nil { - log.Error("GetAttachmentByUUID failed:", err.Error(), ctx.Data["MsgID"]) + log.Error("GetDatasetInfo failed: %v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr("dataset is not exist", tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.error.dataset_select"), tplGrampusTrainJobGPUNew, &form) return } @@ -336,8 +336,22 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain return } + var datasetRemotePath, allFileName string + for _, datasetInfo := range datasetInfos { + if datasetRemotePath == "" { + datasetRemotePath = datasetInfo.DataLocalPath + allFileName = datasetInfo.FullName + } else { + datasetRemotePath = datasetRemotePath + ";" + datasetInfo.DataLocalPath + allFileName = allFileName + ";" + datasetInfo.FullName + } + + } + //prepare command - command, err := generateCommand(repo.Name, grampus.ProcessorTypeGPU, codeMinioPath+cloudbrain.DefaultBranchName+".zip", dataMinioPath, bootFile, params, setting.CBCodePathPrefix+jobName+cloudbrain.ModelMountPath+"/", attachment.Name) + preTrainModelPath := getPreTrainModelPath(form.PreTrainModelUrl, form.CkptName) + + command, err := generateCommand(repo.Name, grampus.ProcessorTypeGPU, codeMinioPath+cloudbrain.DefaultBranchName+".zip", datasetRemotePath, bootFile, params, setting.CBCodePathPrefix+jobName+cloudbrain.ModelMountPath+"/", allFileName, preTrainModelPath, form.CkptName) if err != nil { log.Error("Failed to generateCommand: %s (%v)", displayJobName, err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) @@ -348,26 +362,37 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain commitID, _ := ctx.Repo.GitRepo.GetBranchCommitID(branchName) req := &grampus.GenerateTrainJobReq{ - JobName: jobName, - DisplayJobName: displayJobName, - ComputeResource: models.GPUResource, - ProcessType: grampus.ProcessorTypeGPU, - Command: command, - ImageUrl: image, - Description: description, - BootFile: bootFile, - Uuid: uuid, - CommitID: commitID, - BranchName: branchName, - Params: form.Params, - EngineName: image, - DatasetName: attachment.Name, + JobName: jobName, + DisplayJobName: displayJobName, + ComputeResource: models.GPUResource, + ProcessType: grampus.ProcessorTypeGPU, + Command: command, + ImageUrl: image, + Description: description, + BootFile: bootFile, + Uuid: uuid, + CommitID: commitID, + BranchName: branchName, + Params: form.Params, + EngineName: image, + DatasetNames: datasetNames, + DatasetInfos: datasetInfos, + IsLatestVersion: modelarts.IsLatestVersion, VersionCount: modelarts.VersionCountOne, WorkServerNumber: 1, Spec: spec, } + if form.ModelName != "" { //使用预训练模型训练 + req.ModelName = form.ModelName + req.LabelName = form.LabelName + req.CkptName = form.CkptName + req.ModelVersion = form.ModelVersion + req.PreTrainingModelUrl = form.PreTrainModelUrl + + } + err = grampus.GenerateTrainJob(ctx, req) if err != nil { log.Error("GenerateTrainJob failed:%v", err.Error(), ctx.Data["MsgID"]) @@ -378,6 +403,17 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/modelarts/train-job") } +func getPreTrainModelPath(pretrainModelDir string, fileName string) string { + index := strings.Index(pretrainModelDir, "/") + if index > 0 { + filterBucket := pretrainModelDir[index+1:] + return filterBucket + fileName + } else { + return "" + } + +} + func checkSpecialPool(ctx *context.Context, resourceType string) string { grampus.InitSpecialPool() if grampus.SpecialPools != nil { @@ -410,7 +446,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain repo := ctx.Repo.Repository codeLocalPath := setting.JobPath + jobName + modelarts.CodePath codeObsPath := grampus.JobPath + jobName + modelarts.CodePath - dataObsPath := setting.BasePath + path.Join(uuid[0:1], uuid[1:2]) + "/" + uuid + "/" + //dataObsPath := setting.BasePath + path.Join(uuid[0:1], uuid[1:2]) + "/" + uuid + "/" branchName := form.BranchName isLatestVersion := modelarts.IsLatestVersion versionCount := modelarts.VersionCountOne @@ -492,11 +528,11 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain } //check dataset - attachment, err := models.GetAttachmentByUUID(uuid) + datasetInfos, datasetNames, err := models.GetDatasetInfo(uuid, models.NPU) if err != nil { - log.Error("GetAttachmentByUUID failed:", err.Error(), ctx.Data["MsgID"]) + log.Error("GetDatasetInfo failed: %v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr("dataset is not exist", tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.error.dataset_select"), tplGrampusTrainJobNPUNew, &form) return } @@ -528,8 +564,21 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain return } + var datasetRemotePath, allFileName string + for _, datasetInfo := range datasetInfos { + if datasetRemotePath == "" { + datasetRemotePath = datasetInfo.DataLocalPath + "'" + datasetInfo.FullName + "'" + allFileName = datasetInfo.FullName + } else { + datasetRemotePath = datasetRemotePath + ";" + datasetInfo.DataLocalPath + "'" + datasetInfo.FullName + "'" + allFileName = allFileName + ";" + datasetInfo.FullName + } + + } + //prepare command - command, err := generateCommand(repo.Name, grampus.ProcessorTypeNPU, codeObsPath+cloudbrain.DefaultBranchName+".zip", dataObsPath+"'"+attachment.Name+"'", bootFile, params, setting.CodePathPrefix+jobName+modelarts.OutputPath, attachment.Name) + preTrainModelPath := getPreTrainModelPath(form.PreTrainModelUrl, form.CkptName) + command, err := generateCommand(repo.Name, grampus.ProcessorTypeNPU, codeObsPath+cloudbrain.DefaultBranchName+".zip", datasetRemotePath, bootFile, params, setting.CodePathPrefix+jobName+modelarts.OutputPath, allFileName, preTrainModelPath, form.CkptName) if err != nil { log.Error("Failed to generateCommand: %s (%v)", displayJobName, err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) @@ -546,7 +595,6 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain ProcessType: grampus.ProcessorTypeNPU, Command: command, ImageId: form.ImageID, - DataUrl: dataObsPath, Description: description, CodeObsPath: codeObsPath, BootFileUrl: codeObsPath + bootFile, @@ -560,9 +608,18 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain EngineName: engineName, VersionCount: versionCount, TotalVersionCount: modelarts.TotalVersionCount, - DatasetName: attachment.Name, + DatasetNames: datasetNames, + DatasetInfos: datasetInfos, Spec: spec, } + if form.ModelName != "" { //使用预训练模型训练 + req.ModelName = form.ModelName + req.LabelName = form.LabelName + req.CkptName = form.CkptName + req.ModelVersion = form.ModelVersion + req.PreTrainingModelUrl = form.PreTrainModelUrl + + } err = grampus.GenerateTrainJob(ctx, req) if err != nil { @@ -776,7 +833,7 @@ func GrampusGetLog(ctx *context.Context) { return } -func generateCommand(repoName, processorType, codeRemotePath, dataRemotePath, bootFile, paramSrc, outputRemotePath, datasetName string) (string, error) { +func generateCommand(repoName, processorType, codeRemotePath, dataRemotePath, bootFile, paramSrc, outputRemotePath, datasetName, pretrainModelPath, pretrainModelFileName string) (string, error) { var command string workDir := grampus.NpuWorkDir @@ -788,18 +845,18 @@ func generateCommand(repoName, processorType, codeRemotePath, dataRemotePath, bo //download code & dataset if processorType == grampus.ProcessorTypeNPU { commandDownload := "./downloader_for_obs " + setting.Bucket + " " + codeRemotePath + " " + grampus.CodeArchiveName + " " + dataRemotePath + " '" + datasetName + "';" + commandDownload = processPretrainModelParameter(pretrainModelPath, pretrainModelFileName, commandDownload) command += commandDownload } else if processorType == grampus.ProcessorTypeGPU { - commandDownload := "./downloader_for_minio " + setting.Grampus.Env + " " + codeRemotePath + " " + grampus.CodeArchiveName + " " + dataRemotePath + " '" + datasetName + "';" + commandDownload := "./downloader_for_minio " + setting.Grampus.Env + " " + codeRemotePath + " " + grampus.CodeArchiveName + " " + dataRemotePath + " '" + datasetName + "'" + commandDownload = processPretrainModelParameter(pretrainModelPath, pretrainModelFileName, commandDownload) command += commandDownload } //unzip code & dataset - toolUnzip := "unzip -q '" - if strings.HasSuffix(datasetName, ".tar.gz") { - toolUnzip = "tar -zxvf '" - } - commandUnzip := "cd " + workDir + "code;unzip -q master.zip;echo \"start to unzip dataset\";cd " + workDir + "dataset;" + toolUnzip + datasetName + "';" + unZipDatasetCommand := generateDatasetUnzipCommand(datasetName) + + commandUnzip := "cd " + workDir + "code;unzip -q master.zip;echo \"start to unzip dataset\";cd " + workDir + "dataset;" + unZipDatasetCommand command += commandUnzip command += "echo \"unzip finished;start to exec code;\";" @@ -859,6 +916,38 @@ func generateCommand(repoName, processorType, codeRemotePath, dataRemotePath, bo return command, nil } +func processPretrainModelParameter(pretrainModelPath string, pretrainModelFileName string, commandDownload string) string { + commandDownloadTemp := commandDownload + if pretrainModelPath != "" { + commandDownloadTemp += " '" + pretrainModelPath + "' '" + pretrainModelFileName + "'" + } + commandDownloadTemp += ";" + return commandDownloadTemp +} + +func generateDatasetUnzipCommand(datasetName string) string { + var unZipDatasetCommand string + + datasetNameArray := strings.Split(datasetName, ";") + if len(datasetNameArray) == 1 { //单数据集 + unZipDatasetCommand = "unzip -q '" + datasetName + "';" + if strings.HasSuffix(datasetName, ".tar.gz") { + unZipDatasetCommand = "tar --strip-components=1 -zxvf '" + datasetName + "';" + } + + } else { //多数据集 + for _, datasetNameTemp := range datasetNameArray { + if strings.HasSuffix(datasetName, ".tar.gz") { + unZipDatasetCommand = unZipDatasetCommand + "tar -zxvf '" + datasetName + "';" + } else { + unZipDatasetCommand = unZipDatasetCommand + "unzip -q '" + datasetNameTemp + "' -d './" + strings.TrimSuffix(datasetNameTemp, ".zip") + "';" + } + } + + } + return unZipDatasetCommand +} + func downloadZipCode(ctx *context.Context, codePath, branchName string) error { archiveType := git.ZIP archivePath := codePath From 9df8fa9f24032d1cae07f8f034f5dc233a2f126d Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Tue, 13 Sep 2022 14:48:34 +0800 Subject: [PATCH 132/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/modelarts/modelarts.go | 132 ++++++++++++++++++--------------- routers/repo/modelarts.go | 16 ++++ 2 files changed, 87 insertions(+), 61 deletions(-) diff --git a/modules/modelarts/modelarts.go b/modules/modelarts/modelarts.go index 4539699ad..ead824b60 100755 --- a/modules/modelarts/modelarts.go +++ b/modules/modelarts/modelarts.go @@ -75,35 +75,40 @@ var ( ) type GenerateTrainJobReq struct { - JobName string - DisplayJobName string - Uuid string - Description string - CodeObsPath string - BootFile string - BootFileUrl string - DataUrl string - TrainUrl string - LogUrl string - PoolID string - WorkServerNumber int - EngineID int64 - Parameters []models.Parameter - CommitID string - IsLatestVersion string - Params string - BranchName string - PreVersionId int64 - PreVersionName string - FlavorCode string - FlavorName string - VersionCount int - EngineName string - TotalVersionCount int - UserImageUrl string - UserCommand string - DatasetName string - Spec *models.Specification + JobName string + DisplayJobName string + Uuid string + Description string + CodeObsPath string + BootFile string + BootFileUrl string + DataUrl string + TrainUrl string + LogUrl string + PoolID string + WorkServerNumber int + EngineID int64 + Parameters []models.Parameter + CommitID string + IsLatestVersion string + Params string + BranchName string + PreVersionId int64 + PreVersionName string + FlavorCode string + FlavorName string + VersionCount int + EngineName string + TotalVersionCount int + UserImageUrl string + UserCommand string + DatasetName string + Spec *models.Specification + ModelName string + LabelName string + CkptName string + ModelVersion string + PreTrainingModelUrl string } type GenerateInferenceJobReq struct { @@ -407,38 +412,43 @@ func GenerateTrainJob(ctx *context.Context, req *GenerateTrainJobReq) (err error } jobId := strconv.FormatInt(jobResult.JobID, 10) createErr = models.CreateCloudbrain(&models.Cloudbrain{ - Status: TransTrainJobStatus(jobResult.Status), - UserID: ctx.User.ID, - RepoID: ctx.Repo.Repository.ID, - JobID: jobId, - JobName: req.JobName, - DisplayJobName: req.DisplayJobName, - JobType: string(models.JobTypeTrain), - Type: models.TypeCloudBrainTwo, - VersionID: jobResult.VersionID, - VersionName: jobResult.VersionName, - Uuid: req.Uuid, - DatasetName: req.DatasetName, - CommitID: req.CommitID, - IsLatestVersion: req.IsLatestVersion, - ComputeResource: models.NPUResource, - EngineID: req.EngineID, - TrainUrl: req.TrainUrl, - BranchName: req.BranchName, - Parameters: req.Params, - BootFile: req.BootFile, - DataUrl: req.DataUrl, - LogUrl: req.LogUrl, - FlavorCode: req.Spec.SourceSpecId, - Description: req.Description, - WorkServerNumber: req.WorkServerNumber, - FlavorName: req.FlavorName, - EngineName: req.EngineName, - VersionCount: req.VersionCount, - TotalVersionCount: req.TotalVersionCount, - CreatedUnix: createTime, - UpdatedUnix: createTime, - Spec: req.Spec, + Status: TransTrainJobStatus(jobResult.Status), + UserID: ctx.User.ID, + RepoID: ctx.Repo.Repository.ID, + JobID: jobId, + JobName: req.JobName, + DisplayJobName: req.DisplayJobName, + JobType: string(models.JobTypeTrain), + Type: models.TypeCloudBrainTwo, + VersionID: jobResult.VersionID, + VersionName: jobResult.VersionName, + Uuid: req.Uuid, + DatasetName: req.DatasetName, + CommitID: req.CommitID, + IsLatestVersion: req.IsLatestVersion, + ComputeResource: models.NPUResource, + EngineID: req.EngineID, + TrainUrl: req.TrainUrl, + BranchName: req.BranchName, + Parameters: req.Params, + BootFile: req.BootFile, + DataUrl: req.DataUrl, + LogUrl: req.LogUrl, + FlavorCode: req.Spec.SourceSpecId, + Description: req.Description, + WorkServerNumber: req.WorkServerNumber, + FlavorName: req.FlavorName, + EngineName: req.EngineName, + VersionCount: req.VersionCount, + TotalVersionCount: req.TotalVersionCount, + CreatedUnix: createTime, + UpdatedUnix: createTime, + Spec: req.Spec, + ModelName: req.ModelName, + ModelVersion: req.ModelVersion, + LabelName: req.LabelName, + PreTrainingModelUrl: req.PreTrainingModelUrl, + CkptName: req.CkptName, }) if createErr != nil { diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index b4f6f000e..121c4fd78 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -1290,6 +1290,13 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) Value: string(jsondatas), }) } + if form.ModelName != "" { //使用预训练模型训练 + ckptUrl := "/" + form.PreTrainModelUrl + form.CkptName + param = append(param, models.Parameter{ + Label: modelarts.CkptUrl, + Value: "s3:/" + ckptUrl, + }) + } //save param config // if isSaveParam == "on" { @@ -1358,6 +1365,15 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) DatasetName: datasetNames, Spec: spec, } + if form.ModelName != "" { //使用预训练模型训练 + req.ModelName = form.ModelName + req.LabelName = form.LabelName + req.CkptName = form.CkptName + req.ModelVersion = form.ModelVersion + req.PreTrainingModelUrl = form.PreTrainModelUrl + + } + userCommand, userImageUrl := getUserCommand(engineID, req) req.UserCommand = userCommand req.UserImageUrl = userImageUrl From e24b67cfa97bfca382801751eb4f3e9c38904482 Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Tue, 13 Sep 2022 15:16:39 +0800 Subject: [PATCH 133/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/cloudbrain.go | 38 ++++----- modules/cloudbrain/cloudbrain.go | 6 +- modules/grampus/grampus.go | 84 +++++++++--------- modules/modelarts/modelarts.go | 142 +++++++++++++++---------------- routers/repo/cloudbrain.go | 2 +- routers/repo/grampus.go | 4 +- routers/repo/modelarts.go | 9 +- 7 files changed, 146 insertions(+), 139 deletions(-) diff --git a/models/cloudbrain.go b/models/cloudbrain.go index 12c76ce57..f93b653e1 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -171,25 +171,25 @@ type Cloudbrain struct { ImageID string //grampus image_id AiCenter string //grampus ai center: center_id+center_name - TrainUrl string //输出模型的obs路径 - BranchName string //分支名称 - Parameters string //传给modelarts的param参数 - BootFile string //启动文件 - DataUrl string //数据集的obs路径 - LogUrl string //日志输出的obs路径 - PreVersionId int64 //父版本的版本id - FlavorCode string //modelarts上的规格id - Description string `xorm:"varchar(256)"` //描述 - WorkServerNumber int //节点数 - FlavorName string //规格名称 - EngineName string //引擎名称 - TotalVersionCount int //任务的所有版本数量,包括删除的 - LabelName string //标签名称 - ModelName string //模型名称 - ModelVersion string //模型版本 - CkptName string //权重文件名称 - PreTrainingModelUrl string //预训练模型地址 - ResultUrl string //推理结果的obs路径 + TrainUrl string //输出模型的obs路径 + BranchName string //分支名称 + Parameters string //传给modelarts的param参数 + BootFile string //启动文件 + DataUrl string //数据集的obs路径 + LogUrl string //日志输出的obs路径 + PreVersionId int64 //父版本的版本id + FlavorCode string //modelarts上的规格id + Description string `xorm:"varchar(256)"` //描述 + WorkServerNumber int //节点数 + FlavorName string //规格名称 + EngineName string //引擎名称 + TotalVersionCount int //任务的所有版本数量,包括删除的 + LabelName string //标签名称 + ModelName string //模型名称 + ModelVersion string //模型版本 + CkptName string //权重文件名称 + PreTrainModelUrl string //预训练模型地址 + ResultUrl string //推理结果的obs路径 User *User `xorm:"-"` Repo *Repository `xorm:"-"` diff --git a/modules/cloudbrain/cloudbrain.go b/modules/cloudbrain/cloudbrain.go index 5a4d2fe05..4e527b6bf 100755 --- a/modules/cloudbrain/cloudbrain.go +++ b/modules/cloudbrain/cloudbrain.go @@ -79,7 +79,7 @@ type GenerateCloudBrainTaskReq struct { CkptName string LabelName string PreTrainModelPath string - PreTrainingModelUrl string + PreTrainModelUrl string Spec *models.Specification } @@ -279,7 +279,7 @@ func GenerateTask(req GenerateCloudBrainTaskReq) error { }, } - if req.PreTrainingModelUrl != "" { //预训练 + if req.PreTrainModelUrl != "" { //预训练 volumes = append(volumes, models.Volume{ HostPath: models.StHostPath{ Path: req.PreTrainModelPath, @@ -372,7 +372,7 @@ func GenerateTask(req GenerateCloudBrainTaskReq) error { CkptName: req.CkptName, ResultUrl: req.ResultPath, LabelName: req.LabelName, - PreTrainingModelUrl: req.PreTrainingModelUrl, + PreTrainModelUrl: req.PreTrainModelUrl, CreatedUnix: createTime, UpdatedUnix: createTime, CommitID: req.CommitID, diff --git a/modules/grampus/grampus.go b/modules/grampus/grampus.go index f434a484c..45c127141 100755 --- a/modules/grampus/grampus.go +++ b/modules/grampus/grampus.go @@ -63,16 +63,16 @@ type GenerateTrainJobReq struct { ComputeResource string ProcessType string - DatasetNames string - DatasetInfos map[string]models.DatasetInfo - Params string - ModelName string - LabelName string - CkptName string - ModelVersion string - PreTrainModelPath string - PreTrainingModelUrl string - Spec *models.Specification + DatasetNames string + DatasetInfos map[string]models.DatasetInfo + Params string + ModelName string + LabelName string + CkptName string + ModelVersion string + PreTrainModelPath string + PreTrainModelUrl string + Spec *models.Specification } func GenerateTrainJob(ctx *context.Context, req *GenerateTrainJobReq) (err error) { @@ -102,38 +102,38 @@ func GenerateTrainJob(ctx *context.Context, req *GenerateTrainJobReq) (err error jobID := jobResult.JobInfo.JobID err = models.CreateCloudbrain(&models.Cloudbrain{ - Status: TransTrainJobStatus(jobResult.JobInfo.Status), - UserID: ctx.User.ID, - RepoID: ctx.Repo.Repository.ID, - JobID: jobID, - JobName: req.JobName, - DisplayJobName: req.DisplayJobName, - JobType: string(models.JobTypeTrain), - Type: models.TypeC2Net, - Uuid: req.Uuid, - DatasetName: req.DatasetNames, - CommitID: req.CommitID, - IsLatestVersion: req.IsLatestVersion, - ComputeResource: req.ComputeResource, - ImageID: req.ImageId, - TrainUrl: req.TrainUrl, - BranchName: req.BranchName, - Parameters: req.Params, - BootFile: req.BootFile, - DataUrl: req.DataUrl, - Description: req.Description, - WorkServerNumber: req.WorkServerNumber, - EngineName: req.EngineName, - VersionCount: req.VersionCount, - TotalVersionCount: req.TotalVersionCount, - CreatedUnix: createTime, - UpdatedUnix: createTime, - Spec: req.Spec, - ModelName: req.ModelName, - ModelVersion: req.ModelVersion, - LabelName: req.LabelName, - PreTrainingModelUrl: req.PreTrainingModelUrl, - CkptName: req.CkptName, + Status: TransTrainJobStatus(jobResult.JobInfo.Status), + UserID: ctx.User.ID, + RepoID: ctx.Repo.Repository.ID, + JobID: jobID, + JobName: req.JobName, + DisplayJobName: req.DisplayJobName, + JobType: string(models.JobTypeTrain), + Type: models.TypeC2Net, + Uuid: req.Uuid, + DatasetName: req.DatasetNames, + CommitID: req.CommitID, + IsLatestVersion: req.IsLatestVersion, + ComputeResource: req.ComputeResource, + ImageID: req.ImageId, + TrainUrl: req.TrainUrl, + BranchName: req.BranchName, + Parameters: req.Params, + BootFile: req.BootFile, + DataUrl: req.DataUrl, + Description: req.Description, + WorkServerNumber: req.WorkServerNumber, + EngineName: req.EngineName, + VersionCount: req.VersionCount, + TotalVersionCount: req.TotalVersionCount, + CreatedUnix: createTime, + UpdatedUnix: createTime, + Spec: req.Spec, + ModelName: req.ModelName, + ModelVersion: req.ModelVersion, + LabelName: req.LabelName, + PreTrainModelUrl: req.PreTrainModelUrl, + CkptName: req.CkptName, }) if err != nil { diff --git a/modules/modelarts/modelarts.go b/modules/modelarts/modelarts.go index ead824b60..f35601191 100755 --- a/modules/modelarts/modelarts.go +++ b/modules/modelarts/modelarts.go @@ -75,40 +75,40 @@ var ( ) type GenerateTrainJobReq struct { - JobName string - DisplayJobName string - Uuid string - Description string - CodeObsPath string - BootFile string - BootFileUrl string - DataUrl string - TrainUrl string - LogUrl string - PoolID string - WorkServerNumber int - EngineID int64 - Parameters []models.Parameter - CommitID string - IsLatestVersion string - Params string - BranchName string - PreVersionId int64 - PreVersionName string - FlavorCode string - FlavorName string - VersionCount int - EngineName string - TotalVersionCount int - UserImageUrl string - UserCommand string - DatasetName string - Spec *models.Specification - ModelName string - LabelName string - CkptName string - ModelVersion string - PreTrainingModelUrl string + JobName string + DisplayJobName string + Uuid string + Description string + CodeObsPath string + BootFile string + BootFileUrl string + DataUrl string + TrainUrl string + LogUrl string + PoolID string + WorkServerNumber int + EngineID int64 + Parameters []models.Parameter + CommitID string + IsLatestVersion string + Params string + BranchName string + PreVersionId int64 + PreVersionName string + FlavorCode string + FlavorName string + VersionCount int + EngineName string + TotalVersionCount int + UserImageUrl string + UserCommand string + DatasetName string + Spec *models.Specification + ModelName string + LabelName string + CkptName string + ModelVersion string + PreTrainModelUrl string } type GenerateInferenceJobReq struct { @@ -412,43 +412,43 @@ func GenerateTrainJob(ctx *context.Context, req *GenerateTrainJobReq) (err error } jobId := strconv.FormatInt(jobResult.JobID, 10) createErr = models.CreateCloudbrain(&models.Cloudbrain{ - Status: TransTrainJobStatus(jobResult.Status), - UserID: ctx.User.ID, - RepoID: ctx.Repo.Repository.ID, - JobID: jobId, - JobName: req.JobName, - DisplayJobName: req.DisplayJobName, - JobType: string(models.JobTypeTrain), - Type: models.TypeCloudBrainTwo, - VersionID: jobResult.VersionID, - VersionName: jobResult.VersionName, - Uuid: req.Uuid, - DatasetName: req.DatasetName, - CommitID: req.CommitID, - IsLatestVersion: req.IsLatestVersion, - ComputeResource: models.NPUResource, - EngineID: req.EngineID, - TrainUrl: req.TrainUrl, - BranchName: req.BranchName, - Parameters: req.Params, - BootFile: req.BootFile, - DataUrl: req.DataUrl, - LogUrl: req.LogUrl, - FlavorCode: req.Spec.SourceSpecId, - Description: req.Description, - WorkServerNumber: req.WorkServerNumber, - FlavorName: req.FlavorName, - EngineName: req.EngineName, - VersionCount: req.VersionCount, - TotalVersionCount: req.TotalVersionCount, - CreatedUnix: createTime, - UpdatedUnix: createTime, - Spec: req.Spec, - ModelName: req.ModelName, - ModelVersion: req.ModelVersion, - LabelName: req.LabelName, - PreTrainingModelUrl: req.PreTrainingModelUrl, - CkptName: req.CkptName, + Status: TransTrainJobStatus(jobResult.Status), + UserID: ctx.User.ID, + RepoID: ctx.Repo.Repository.ID, + JobID: jobId, + JobName: req.JobName, + DisplayJobName: req.DisplayJobName, + JobType: string(models.JobTypeTrain), + Type: models.TypeCloudBrainTwo, + VersionID: jobResult.VersionID, + VersionName: jobResult.VersionName, + Uuid: req.Uuid, + DatasetName: req.DatasetName, + CommitID: req.CommitID, + IsLatestVersion: req.IsLatestVersion, + ComputeResource: models.NPUResource, + EngineID: req.EngineID, + TrainUrl: req.TrainUrl, + BranchName: req.BranchName, + Parameters: req.Params, + BootFile: req.BootFile, + DataUrl: req.DataUrl, + LogUrl: req.LogUrl, + FlavorCode: req.Spec.SourceSpecId, + Description: req.Description, + WorkServerNumber: req.WorkServerNumber, + FlavorName: req.FlavorName, + EngineName: req.EngineName, + VersionCount: req.VersionCount, + TotalVersionCount: req.TotalVersionCount, + CreatedUnix: createTime, + UpdatedUnix: createTime, + Spec: req.Spec, + ModelName: req.ModelName, + ModelVersion: req.ModelVersion, + LabelName: req.LabelName, + PreTrainModelUrl: req.PreTrainModelUrl, + CkptName: req.CkptName, }) if createErr != nil { diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 56a485b66..8cd45e06f 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -334,7 +334,7 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { req.CkptName = form.CkptName req.ModelVersion = form.ModelVersion req.PreTrainModelPath = setting.Attachment.Minio.RealPath + form.PreTrainModelUrl - req.PreTrainingModelUrl = form.PreTrainModelUrl + req.PreTrainModelUrl = form.PreTrainModelUrl } diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index b32070a84..ed869e76e 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -389,7 +389,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain req.LabelName = form.LabelName req.CkptName = form.CkptName req.ModelVersion = form.ModelVersion - req.PreTrainingModelUrl = form.PreTrainModelUrl + req.PreTrainModelUrl = form.PreTrainModelUrl } @@ -617,7 +617,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain req.LabelName = form.LabelName req.CkptName = form.CkptName req.ModelVersion = form.ModelVersion - req.PreTrainingModelUrl = form.PreTrainModelUrl + req.PreTrainModelUrl = form.PreTrainModelUrl } diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index 121c4fd78..cb4b2c1cc 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -1010,6 +1010,13 @@ func trainJobNewVersionDataPrepare(ctx *context.Context) error { ctx.Data["engine_id"] = task.EngineID ctx.Data["datasetType"] = models.TypeCloudBrainTwo + //pretrain model + ctx.Data["model_name"] = task.ModelName + ctx.Data["model_version"] = task.ModelVersion + ctx.Data["ckpt_name"] = task.CkptName + ctx.Data["label_names"] = task.LabelName + ctx.Data["pre_train_model_url"] = task.PreTrainModelUrl + configList, err := getConfigList(modelarts.PerPage, 1, modelarts.SortByCreateTime, "desc", "", modelarts.ConfigTypeCustom) if err != nil { ctx.ServerError("getConfigList failed:", err) @@ -1370,7 +1377,7 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm) req.LabelName = form.LabelName req.CkptName = form.CkptName req.ModelVersion = form.ModelVersion - req.PreTrainingModelUrl = form.PreTrainModelUrl + req.PreTrainModelUrl = form.PreTrainModelUrl } From 9ce1b017b7bab8858f1ff45b2390b492ecfd56c4 Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Tue, 13 Sep 2022 16:32:30 +0800 Subject: [PATCH 134/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/grampus/grampus.go | 6 +++--- modules/setting/setting.go | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/grampus/grampus.go b/modules/grampus/grampus.go index 45c127141..3cdd59c5c 100755 --- a/modules/grampus/grampus.go +++ b/modules/grampus/grampus.go @@ -22,9 +22,6 @@ const ( GpuWorkDir = "/tmp/" NpuWorkDir = "/cache/" - CommandPrepareScript = ";mkdir -p output;mkdir -p code;mkdir -p dataset;echo \"start loading script\";wget -q https://git.openi.org.cn/OpenIOSSG/script_for_grampus/archive/master.zip;" + - "echo \"finish loading script\";unzip -q master.zip;cd script_for_grampus;chmod 777 downloader_for_obs uploader_for_npu downloader_for_minio uploader_for_gpu;" - CodeArchiveName = "master.zip" ) @@ -34,6 +31,9 @@ var ( ImageInfos *setting.StImageInfosModelArts SpecialPools *models.SpecialPools + + CommandPrepareScript = ";mkdir -p output;mkdir -p code;mkdir -p dataset;mkdir -p pretrainmodel;echo \"start loading script\";wget -q https://git.openi.org.cn/OpenIOSSG/" + setting.Grampus.SyncScriptProject + "/archive/master.zip;" + + "echo \"finish loading script\";unzip -q master.zip;cd script_for_grampus;chmod 777 downloader_for_obs uploader_for_npu downloader_for_minio uploader_for_gpu;" ) type GenerateTrainJobReq struct { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 1992baf54..7d726a773 100755 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -583,12 +583,13 @@ var ( //grampus config Grampus = struct { - Env string - Host string - UserName string - Password string - SpecialPools string - C2NetSequence string + Env string + Host string + UserName string + Password string + SpecialPools string + C2NetSequence string + SyncScriptProject string }{} C2NetInfos *C2NetSqInfos @@ -1558,6 +1559,8 @@ func getGrampusConfig() { log.Error("Unmarshal(C2NetSequence) failed:%v", err) } } + Grampus.SyncScriptProject = sec.Key("SYNC_SCRIPT_PROJECT").MustString("script_for_grampus") + } func SetRadarMapConfig() { From 041cdec7304512b9416e0de77a44129f36ee6751 Mon Sep 17 00:00:00 2001 From: liuzx Date: Tue, 13 Sep 2022 16:56:49 +0800 Subject: [PATCH 135/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routers/repo/cloudbrain.go | 38 +++---------------- routers/repo/grampus.go | 17 +++++---- .../repo/cloudbrain/trainjob/versionnew.tmpl | 0 3 files changed, 15 insertions(+), 40 deletions(-) create mode 100644 templates/repo/cloudbrain/trainjob/versionnew.tmpl diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index ec3bf1f39..d870d575c 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -49,8 +49,9 @@ const ( tplCloudBrainImageSubmit base.TplName = "repo/cloudbrain/image/submit" tplCloudBrainImageEdit base.TplName = "repo/cloudbrain/image/edit" - tplCloudBrainTrainJobNew base.TplName = "repo/cloudbrain/trainjob/new" - tplCloudBrainTrainJobShow base.TplName = "repo/cloudbrain/trainjob/show" + tplCloudBrainTrainJobNew base.TplName = "repo/cloudbrain/trainjob/new" + tplCloudBrainTrainJobVersionNew base.TplName = "repo/cloudbrain/trainjob/versionnew" + tplCloudBrainTrainJobShow base.TplName = "repo/cloudbrain/trainjob/show" tplCloudBrainInferenceJobNew base.TplName = "repo/cloudbrain/inference/new" tplCloudBrainInferenceJobShow base.TplName = "repo/cloudbrain/inference/show" @@ -146,33 +147,6 @@ func cloudBrainVersionNewDataPrepare(ctx *context.Context) error { t := time.Now() var displayJobName = jobNamePrefixValid(cutString(ctx.User.Name, 5)) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[5:] ctx.Data["display_job_name"] = displayJobName - - ctx.Data["command"] = cloudbrain.GetCloudbrainDebugCommand() - ctx.Data["code_path"] = cloudbrain.CodeMountPath - ctx.Data["dataset_path"] = cloudbrain.DataSetMountPath - ctx.Data["model_path"] = cloudbrain.ModelMountPath - ctx.Data["benchmark_path"] = cloudbrain.BenchMarkMountPath - ctx.Data["is_benchmark_enabled"] = setting.IsBenchmarkEnabled - - if categories == nil { - json.Unmarshal([]byte(setting.BenchmarkCategory), &categories) - } - ctx.Data["benchmark_categories"] = categories.Category - - ctx.Data["benchmark_types"] = GetBenchmarkTypes(ctx).BenchmarkType - queuesDetail, _ := cloudbrain.GetQueuesDetail() - if queuesDetail != nil { - ctx.Data["QueuesDetail"] = queuesDetail - } - - prepareCloudbrainOneSpecs(ctx) - - ctx.Data["snn4imagenet_path"] = cloudbrain.Snn4imagenetMountPath - ctx.Data["is_snn4imagenet_enabled"] = setting.IsSnn4imagenetEnabled - ctx.Data["brainscore_path"] = cloudbrain.BrainScoreMountPath - ctx.Data["is_brainscore_enabled"] = setting.IsBrainScoreEnabled - ctx.Data["benchmarkMode"] = ctx.Query("benchmarkMode") - ctx.Data["branch_name"] = ctx.Cloudbrain.BranchName ctx.Data["image_name"] = ctx.Cloudbrain.Image ctx.Data["bootfile"] = ctx.Cloudbrain.BootFile @@ -197,10 +171,10 @@ func cloudBrainVersionNewDataPrepare(ctx *context.Context) error { ctx.Data["dataset_name"] = datasetNames } ctx.Data["uuid"] = ctx.Cloudbrain.Uuid + ctx.Data["cluster_type"] = models.OpenICluster ctx.Data["compute_resource"] = ctx.Cloudbrain.ComputeResource - ctx.Data["dataset_type"] = models.TypeCloudBrainOne - waitCount := cloudbrain.GetWaitingCloudbrainCount(models.TypeC2Net, models.GPUResource, models.JobTypeTrain) + waitCount := cloudbrain.GetWaitingCloudbrainCount(models.TypeCloudBrainOne, models.GPUResource, models.JobTypeTrain) ctx.Data["wait_count"] = waitCount return nil @@ -2624,7 +2598,7 @@ func CloudBrainTrainJobVersionNew(ctx *context.Context) { ctx.ServerError("get new train-job info failed", err) return } - ctx.HTML(http.StatusOK, tplCloudBrainTrainJobNew) + ctx.HTML(http.StatusOK, tplCloudBrainTrainJobVersionNew) } func InferenceCloudBrainJobNew(ctx *context.Context) { diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index f4ab40f5e..0c55067da 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -220,9 +220,9 @@ func grampusTrainJobVersionNewDataPrepare(ctx *context.Context, processType stri ctx.Data["branches"] = branches } - ctx.Data["BranchName"] = ctx.Cloudbrain.BranchName - ctx.Data["ImageName"] = ctx.Cloudbrain.Image - ctx.Data["BootFile"] = ctx.Cloudbrain.BootFile + ctx.Data["branch_name"] = ctx.Cloudbrain.BranchName + ctx.Data["image_name"] = ctx.Cloudbrain.Image + ctx.Data["bootfile"] = ctx.Cloudbrain.BootFile ctx.Data["description"] = ctx.Cloudbrain.Description spec, _ := resource.GetCloudbrainSpec(ctx.Cloudbrain.ID) if spec != nil { @@ -244,16 +244,17 @@ func grampusTrainJobVersionNewDataPrepare(ctx *context.Context, processType stri ctx.Data["dataset_name"] = datasetNames } ctx.Data["uuid"] = ctx.Cloudbrain.Uuid - ctx.Data["ComputeResource"] = ctx.Cloudbrain.ComputeResource + ctx.Data["cloudbrain_type"] = models.C2NetCluster + ctx.Data["compute_resource"] = ctx.Cloudbrain.ComputeResource if processType == grampus.ProcessorTypeGPU { - ctx.Data["datasetType"] = models.TypeCloudBrainOne + ctx.Data["dataset_type"] = models.TypeCloudBrainOne waitCount := cloudbrain.GetWaitingCloudbrainCount(models.TypeC2Net, models.GPUResource, models.JobTypeTrain) - ctx.Data["WaitCount"] = waitCount + ctx.Data["wait_count"] = waitCount } else if processType == grampus.ProcessorTypeNPU { - ctx.Data["datasetType"] = models.TypeCloudBrainTwo + ctx.Data["dataset_type"] = models.TypeCloudBrainTwo waitCount := cloudbrain.GetWaitingCloudbrainCount(models.TypeC2Net, models.NPUResource, models.JobTypeTrain) - ctx.Data["WaitCount"] = waitCount + ctx.Data["wait_count"] = waitCount ctx.Data["work_server_number"] = ctx.Cloudbrain.WorkServerNumber } diff --git a/templates/repo/cloudbrain/trainjob/versionnew.tmpl b/templates/repo/cloudbrain/trainjob/versionnew.tmpl new file mode 100644 index 000000000..e69de29bb From 601b7873b9f01bf41579a31ccec8a89b74092b58 Mon Sep 17 00:00:00 2001 From: zhoupzh Date: Wed, 14 Sep 2022 12:19:55 +0800 Subject: [PATCH 136/283] fix issue --- templates/custom/select_model.tmpl | 37 +++++ templates/repo/cloudbrain/trainjob/new.tmpl | 4 +- templates/repo/grampus/trainjob/gpu/new.tmpl | 3 +- templates/repo/grampus/trainjob/npu/new.tmpl | 2 + templates/repo/modelarts/trainjob/new.tmpl | 6 +- .../repo/modelarts/trainjob/version_new.tmpl | 4 +- web_src/js/features/cloudbrainShow.js | 130 ++++++++++++++++++ 7 files changed, 178 insertions(+), 8 deletions(-) create mode 100644 templates/custom/select_model.tmpl diff --git a/templates/custom/select_model.tmpl b/templates/custom/select_model.tmpl new file mode 100644 index 000000000..81332b873 --- /dev/null +++ b/templates/custom/select_model.tmpl @@ -0,0 +1,37 @@ + +
+   +
+ +
+
+ +
+
+ + +
+ + + + +
\ No newline at end of file diff --git a/templates/repo/cloudbrain/trainjob/new.tmpl b/templates/repo/cloudbrain/trainjob/new.tmpl index 709490ac1..b2cff22cc 100755 --- a/templates/repo/cloudbrain/trainjob/new.tmpl +++ b/templates/repo/cloudbrain/trainjob/new.tmpl @@ -70,7 +70,7 @@
{{template "repo/header" .}}
- + {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} @@ -168,7 +168,7 @@ {{end}}

- + {{template "custom/select_model" .}} - + {{template "custom/select_model" .}}
diff --git a/templates/repo/grampus/trainjob/npu/new.tmpl b/templates/repo/grampus/trainjob/npu/new.tmpl index 88a41779e..a11d84bb3 100755 --- a/templates/repo/grampus/trainjob/npu/new.tmpl +++ b/templates/repo/grampus/trainjob/npu/new.tmpl @@ -57,6 +57,7 @@
{{template "repo/header" .}}
+ {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} @@ -149,6 +150,7 @@ {{end}}

+ {{template "custom/select_model" .}}
- - + + {{template "custom/select_model" .}}
diff --git a/templates/repo/modelarts/trainjob/version_new.tmpl b/templates/repo/modelarts/trainjob/version_new.tmpl index c95f5699a..ffc1045e8 100644 --- a/templates/repo/modelarts/trainjob/version_new.tmpl +++ b/templates/repo/modelarts/trainjob/version_new.tmpl @@ -55,7 +55,7 @@
{{template "repo/header" .}}
- + {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} @@ -154,7 +154,7 @@

- + {{template "custom/select_model" .}}
diff --git a/web_src/js/features/cloudbrainShow.js b/web_src/js/features/cloudbrainShow.js index 73a8ed7b6..ac1263b9d 100644 --- a/web_src/js/features/cloudbrainShow.js +++ b/web_src/js/features/cloudbrainShow.js @@ -443,4 +443,134 @@ export default async function initCloudrainSow() { html += "
"; $(`#dir_list${version_name}`).append(html); } + + let nameMap, nameList; + let RepoLink = $(".cloudbrain-type").data("repo-link"); + let type = $(".cloudbrain-type").data("cloudbrain-type"); + let flagModel = $(".cloudbrain-type").data("flag-model"); + // 获取模型列表和模型名称对应的模型版本 + $(document).ready(function () { + if (!flagModel) return; + else { + $.get( + `${RepoLink}/modelmanage/query_model_for_predict?type=${type}`, + (data) => { + nameMap = data.nameMap; + nameList = data.nameList; + let html = ""; + nameList.forEach((element) => { + html += `
${element}
`; + }); + if (nameList.length !== 0) { + $("#model_name").append(html); + } + let faildModelName = $('input[name="model_name"]').val(); + let faildModelVersion = $('input[name="model_version"]').val(); + let faildTrainUrl = $('input[name="pre_train_model_url"]').val(); + let faildCkptName = $('input[name="ckpt_name"]').val(); + // 新建错误的表单返回初始化 + if (faildModelName) { + $("#select_model").dropdown("set text", faildModelName); + $("#select_model").dropdown("set value", faildModelName); + $("#select_model_version").dropdown("set text", faildModelVersion); + $("#select_model_version").dropdown("set value", faildTrainUrl); + $("#select_model_checkpoint").dropdown("set text", faildCkptName); + $("#select_model_checkpoint").dropdown("set value", faildCkptName); + } + } + ); + } + $("#select_model").dropdown({ + onChange: function (value, text, $selectedItem) { + $("#model_name_version").empty(); + let html = ""; + nameMap[value].forEach((element) => { + let { TrainTaskInfo } = element; + TrainTaskInfo = JSON.parse(TrainTaskInfo); + html += `
${element.Version}
`; + }); + $("#model_name_version").append(html); + const initVersionText = $( + "#model_name_version div.item:first-child" + ).text(); + const initVersionValue = $( + "#model_name_version div.item:first-child" + ).data("value"); + + $("#select_model_version").dropdown("set text", initVersionText); + $("#select_model_version").dropdown( + "set value", + initVersionValue, + initVersionText, + $("#model_name_version div.item:first-child") + ); + }, + }); + + $("#select_model_version").dropdown({ + onChange: function (value, text, $selectedItem) { + const dataID = + $selectedItem && $selectedItem[0].getAttribute("data-id"); + $("input#ai_model_version").val(text); + $("#select_model_checkpoint").addClass("loading"); + $("#model_checkpoint").empty(); + let html = ""; + loadCheckpointList(dataID).then((res) => { + res.forEach((element) => { + const ckptSuffix = element.FileName.split("."); + const loadCheckpointFile = [ + "ckpt", + "pb", + "h5", + "json", + "pkl", + "pth", + "t7", + "pdparams", + "onnx", + "pbtxt", + "keras", + "mlmodel", + "cfg", + "pt", + ]; + if ( + !element.IsDir && + loadCheckpointFile.includes(ckptSuffix[ckptSuffix.length - 1]) + ) { + html += `
${element.FileName}
`; + } + }); + $("#model_checkpoint").append(html); + $("#select_model_checkpoint").removeClass("loading"); + const initVersionText = $( + "#model_checkpoint div.item:first-child" + ).text(); + const initVersionValue = $( + "#model_checkpoint div.item:first-child" + ).data("value"); + + $("#select_model_checkpoint").dropdown("set text", initVersionText); + $("#select_model_checkpoint").dropdown( + "set value", + initVersionValue, + initVersionText, + $("#model_name_version div.item:first-child") + ); + }); + }, + }); + }); + + function loadCheckpointList(value) { + return new Promise((resolve, reject) => { + $.get( + `${RepoLink}/modelmanage/query_modelfile_for_predict`, + { ID: value }, + (data) => { + resolve(data); + } + ); + }); + } } From 4752033910ed48ad3c6a9fe3cccce61574468f92 Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Wed, 14 Sep 2022 15:07:53 +0800 Subject: [PATCH 137/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routers/repo/grampus.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index ed869e76e..dee8fbdd1 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -887,6 +887,10 @@ func generateCommand(repoName, processorType, codeRemotePath, dataRemotePath, bo } } + if pretrainModelFileName != "" { + paramCode += " --pretrainmodelname" + "=" + pretrainModelFileName + } + var commandCode string if processorType == grampus.ProcessorTypeNPU { commandCode = "/bin/bash /home/work/run_train_for_openi.sh " + workDir + "code/" + strings.ToLower(repoName) + "/" + bootFile + " /tmp/log/train.log" + paramCode + ";" From aea59980292e8de6d1ca043a2f2cb065e48865b9 Mon Sep 17 00:00:00 2001 From: chenshihai Date: Wed, 14 Sep 2022 15:18:12 +0800 Subject: [PATCH 138/283] update tmpl --- templates/repo/modelarts/notebook/new.tmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/repo/modelarts/notebook/new.tmpl b/templates/repo/modelarts/notebook/new.tmpl index 5d56a04f8..f9c4670a5 100755 --- a/templates/repo/modelarts/notebook/new.tmpl +++ b/templates/repo/modelarts/notebook/new.tmpl @@ -91,7 +91,6 @@
{{end}} -
+
+ {{.CsrfTokenHtml}} + + +

{{.i18n.Tr "repo.modelarts.train_job.basic_info"}}:

+ +
+ + +
+
+ + {{template "custom/task_wait_count" .}} +
+ + {{.i18n.Tr "cloudbrain.new_train_gpu_tooltips" "/code" "/dataset" "/model" | Safe}} +
+
+
+ + + {{.i18n.Tr "repo.cloudbrain_jobname_err"}} +
+ +
+ + {{if .description}} + + {{else}} + + {{end}} +
+
+ +

{{.i18n.Tr "repo.modelarts.train_job.parameter_setting"}}:

+ + +
+ + +
+ {{template "custom/select_model" .}} + + +
+ +
+ +
+ + {{if .boot_file}} + + {{else}} + + {{end}} + + + + {{.i18n.Tr "cloudbrain.view_sample"}} +
+
+ +
+ +
+ + {{.i18n.Tr "repo.modelarts.train_job.add_run_parameter"}} + +
+ +
+
+ + +
+ + +
+ +
+ + + {{.i18n.Tr "repo.cloudbrain.cancel"}} +
+ + + +
+
+
+
+{{template "base/footer" .}} + + diff --git a/templates/repo/grampus/trainjob/npu/versionnew.tmpl b/templates/repo/grampus/trainjob/npu/versionnew.tmpl index e69de29bb..8ba1d07ca 100644 --- a/templates/repo/grampus/trainjob/npu/versionnew.tmpl +++ b/templates/repo/grampus/trainjob/npu/versionnew.tmpl @@ -0,0 +1,426 @@ +{{template "base/head" .}} + +{{template "custom/global_mask" .}} +
+ {{template "repo/header" .}} +
+ + {{template "base/alert" .}} +

+ {{.i18n.Tr "repo.modelarts.train_job.new"}} +

+
+ +
+ {{.CsrfTokenHtml}} + + + + + +

{{.i18n.Tr "repo.modelarts.train_job.basic_info"}}:

+ +
+ + +
+
+ + {{template "custom/task_wait_count" .}} +
+ + {{.i18n.Tr "cloudbrain.new_train_gpu_tooltips" "/cache/code" "/cache/dataset" "/cache/output" | Safe}} +
+
+
+ + + {{.i18n.Tr "repo.cloudbrain_jobname_err"}} +
+ +
+ + {{if .description}} + + {{else}} + + {{end}} +
+
+ +

{{.i18n.Tr "repo.modelarts.train_job.parameter_setting"}}:

+ +
+ + +
+ {{template "custom/select_model" .}} +
+ + +
+
+ + {{if .boot_file}} + + {{else}} + + {{end}} + + + + {{.i18n.Tr "cloudbrain.view_sample"}} +
+ + {{template "custom/select_dataset_train" .}} + +
+ + {{.i18n.Tr "repo.modelarts.train_job.add_run_parameter"}} + +
+ +
+
+ + +
+ + +
+
+ + +
+ + +
+ +
+ +
+
+ +
+ + + {{.i18n.Tr "repo.cloudbrain.cancel"}} +
+ + + +
+
+
+
+{{template "base/footer" .}} + + + diff --git a/templates/repo/modelarts/trainjob/index.tmpl b/templates/repo/modelarts/trainjob/index.tmpl index 42c59ba4b..0ac12982b 100755 --- a/templates/repo/modelarts/trainjob/index.tmpl +++ b/templates/repo/modelarts/trainjob/index.tmpl @@ -109,7 +109,6 @@ {{range .Tasks}}
- + + +
+ {{$.CsrfTokenHtml}} + {{if .CanDel}} + + {{$.i18n.Tr "repo.modelarts.modify"}} + + {{else}} + + {{$.i18n.Tr "repo.modelarts.modify"}} + + {{end}} +
@@ -239,6 +253,7 @@ + + From 1793aab66b2d7037e03719f202693ab846041d84 Mon Sep 17 00:00:00 2001 From: zhoupzh Date: Thu, 15 Sep 2022 18:20:56 +0800 Subject: [PATCH 171/283] fix issue --- templates/repo/grampus/trainjob/gpu/new.tmpl | 2 +- .../repo/grampus/trainjob/gpu/versionnew.tmpl | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/templates/repo/grampus/trainjob/gpu/new.tmpl b/templates/repo/grampus/trainjob/gpu/new.tmpl index 530be6b76..d71ed0736 100755 --- a/templates/repo/grampus/trainjob/gpu/new.tmpl +++ b/templates/repo/grampus/trainjob/gpu/new.tmpl @@ -62,7 +62,7 @@
{{template "repo/header" .}}
- + {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} diff --git a/templates/repo/grampus/trainjob/gpu/versionnew.tmpl b/templates/repo/grampus/trainjob/gpu/versionnew.tmpl index 530be6b76..56823f557 100644 --- a/templates/repo/grampus/trainjob/gpu/versionnew.tmpl +++ b/templates/repo/grampus/trainjob/gpu/versionnew.tmpl @@ -62,7 +62,7 @@
{{template "repo/header" .}}
- + {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} @@ -183,7 +183,22 @@ {{.i18n.Tr "repo.modelarts.train_job.add_run_parameter"}}
+ {{if ne 0 (len .params)}} + {{range $k ,$v := .params}} +
+
+ +
+
+ +
+ + + +
+ {{end}} + {{end}}

- + {{.CsrfTokenHtml}} diff --git a/templates/repo/grampus/trainjob/npu/new.tmpl b/templates/repo/grampus/trainjob/npu/new.tmpl index 1105e7638..69f28f47a 100755 --- a/templates/repo/grampus/trainjob/npu/new.tmpl +++ b/templates/repo/grampus/trainjob/npu/new.tmpl @@ -57,7 +57,7 @@
{{template "repo/header" .}}
- + {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} From 19d2fc8c1ac7dcf8fe7b4aa4850b4b043d109e47 Mon Sep 17 00:00:00 2001 From: zhoupzh Date: Thu, 15 Sep 2022 19:08:11 +0800 Subject: [PATCH 173/283] fix issue --- templates/repo/grampus/trainjob/gpu/versionnew.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/grampus/trainjob/gpu/versionnew.tmpl b/templates/repo/grampus/trainjob/gpu/versionnew.tmpl index c3b531c46..2344318e5 100644 --- a/templates/repo/grampus/trainjob/gpu/versionnew.tmpl +++ b/templates/repo/grampus/trainjob/gpu/versionnew.tmpl @@ -74,7 +74,7 @@ - +

{{.i18n.Tr "repo.modelarts.train_job.basic_info"}}:

From 6ced72f8892f2a40b1f4a1e0023c6cb30ceb1fc6 Mon Sep 17 00:00:00 2001 From: zhoupzh Date: Thu, 15 Sep 2022 19:45:37 +0800 Subject: [PATCH 174/283] fix issue --- templates/repo/grampus/trainjob/gpu/new.tmpl | 5 ++--- templates/repo/grampus/trainjob/gpu/versionnew.tmpl | 2 +- templates/repo/grampus/trainjob/npu/new.tmpl | 4 +--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/templates/repo/grampus/trainjob/gpu/new.tmpl b/templates/repo/grampus/trainjob/gpu/new.tmpl index b0b28e33d..3de1d0627 100755 --- a/templates/repo/grampus/trainjob/gpu/new.tmpl +++ b/templates/repo/grampus/trainjob/gpu/new.tmpl @@ -62,7 +62,7 @@
{{template "repo/header" .}}
- + {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} @@ -75,8 +75,7 @@ - - +

{{.i18n.Tr "repo.modelarts.train_job.basic_info"}}:

diff --git a/templates/repo/grampus/trainjob/gpu/versionnew.tmpl b/templates/repo/grampus/trainjob/gpu/versionnew.tmpl index 2344318e5..c3b531c46 100644 --- a/templates/repo/grampus/trainjob/gpu/versionnew.tmpl +++ b/templates/repo/grampus/trainjob/gpu/versionnew.tmpl @@ -74,7 +74,7 @@ - +

{{.i18n.Tr "repo.modelarts.train_job.basic_info"}}:

diff --git a/templates/repo/grampus/trainjob/npu/new.tmpl b/templates/repo/grampus/trainjob/npu/new.tmpl index 69f28f47a..064c0c423 100755 --- a/templates/repo/grampus/trainjob/npu/new.tmpl +++ b/templates/repo/grampus/trainjob/npu/new.tmpl @@ -57,7 +57,7 @@
{{template "repo/header" .}}
- + {{template "base/alert" .}}

{{.i18n.Tr "repo.modelarts.train_job.new"}} @@ -69,8 +69,6 @@ - -

{{.i18n.Tr "repo.modelarts.train_job.basic_info"}}:

From 708bd9a075d2782d8176fbdd8b4e12a02f66ce33 Mon Sep 17 00:00:00 2001 From: liuzx Date: Fri, 16 Sep 2022 09:18:46 +0800 Subject: [PATCH 175/283] update --- options/locale/locale_en-US.ini | 2 +- options/locale/locale_zh-CN.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ac3f5cb9c..8d3868b73 100755 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1093,7 +1093,7 @@ cloudbrain_operate = Operate cloudbrain_status_createtime = Status/Createtime cloudbrain_status_runtime = Running Time cloudbrain_jobname_err=Name must start with a lowercase letter or number,can include lowercase letter,number,_ and -,can not end with _, and can be up to 36 characters long. -cloudbrain_samejob_err=You have created a task with the same name, the system is processing it, please wait a minute. +cloudbrain_samejob_err=A task with the same name has been created, the system is processing it, please wait a minute. cloudbrain_bootfile_err=The bootfile does not exist in the repository cloudbrain_query_fail=Failed to query cloudbrain information. cloudbrain.mirror_tag = Mirror Tag diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index b14882a14..b02600803 100755 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1097,7 +1097,7 @@ cloudbrain_operate=操作 cloudbrain_status_createtime=状态/创建时间 cloudbrain_status_runtime = 运行时长 cloudbrain_jobname_err=只能以小写字母或数字开头且只包含小写字母、数字、_和-,不能以_结尾,最长36个字符。 -cloudbrain_samejob_err=您已经创建了同名任务,系统处理中,请您稍候。 +cloudbrain_samejob_err=同名任务已经被创建,系统处理中,请您稍候。 cloudbrain_bootfile_err=仓库中不存在启动文件 cloudbrain_query_fail=查询云脑任务失败。 cloudbrain.mirror_tag = 镜像标签 From e0ddc24fe2adaf860cfbed45774f29593c28b8f1 Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 16 Sep 2022 09:22:30 +0800 Subject: [PATCH 176/283] #2872 fix bug --- models/cloudbrain.go | 2 +- routers/repo/cloudbrain.go | 2 +- routers/repo/grampus.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/models/cloudbrain.go b/models/cloudbrain.go index fa9a27bb9..f1a99577b 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -278,7 +278,7 @@ func (task *Cloudbrain) IsRunning() bool { } func ConvertDurationToStr(duration int64) string { - if duration == 0 { + if duration <= 0 { return DURATION_STR_ZERO } return util.AddZero(duration/3600) + ":" + util.AddZero(duration%3600/60) + ":" + util.AddZero(duration%60) diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index e58ec45c2..c521626cb 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -1840,7 +1840,6 @@ func SyncCloudbrainStatus() { oldStatus := task.Status task.Status = grampus.TransTrainJobStatus(result.JobInfo.Status) task.Duration = result.JobInfo.RunSec - task.TrainJobDuration = models.ConvertDurationToStr(task.Duration) if task.StartTime == 0 && result.JobInfo.StartedAt > 0 { task.StartTime = timeutil.TimeStamp(result.JobInfo.StartedAt) @@ -1849,6 +1848,7 @@ func SyncCloudbrainStatus() { task.EndTime = task.StartTime.Add(task.Duration) } task.CorrectCreateUnix() + task.ComputeAndSetDuration() if oldStatus != task.Status { notification.NotifyChangeCloudbrainStatus(task, oldStatus) } diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index b204d20da..a677f08a4 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -709,7 +709,6 @@ func GrampusTrainJobShow(ctx *context.Context) { task.Status = grampus.TransTrainJobStatus(result.JobInfo.Status) if task.Status != result.JobInfo.Status || result.JobInfo.Status == models.GrampusStatusRunning { task.Duration = result.JobInfo.RunSec - task.TrainJobDuration = models.ConvertDurationToStr(task.Duration) if task.StartTime == 0 && result.JobInfo.StartedAt > 0 { task.StartTime = timeutil.TimeStamp(result.JobInfo.StartedAt) @@ -718,6 +717,7 @@ func GrampusTrainJobShow(ctx *context.Context) { task.EndTime = task.StartTime.Add(task.Duration) } task.CorrectCreateUnix() + task.ComputeAndSetDuration() if oldStatus != task.Status { notification.NotifyChangeCloudbrainStatus(task, oldStatus) } From 7055faf201eb4812d9e1bc96dbddb8db7608da8c Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Fri, 16 Sep 2022 09:34:33 +0800 Subject: [PATCH 177/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/grampus/grampus.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/grampus/grampus.go b/modules/grampus/grampus.go index ef7e55c65..83fc3b1d4 100755 --- a/modules/grampus/grampus.go +++ b/modules/grampus/grampus.go @@ -80,6 +80,8 @@ func GenerateTrainJob(ctx *context.Context, req *GenerateTrainJobReq) (err error centerID, centerName := getCentersParamter(ctx, req) + log.Info("grampus Command:" + req.Command) + jobResult, err := createJob(models.CreateGrampusJobRequest{ Name: req.JobName, Tasks: []models.GrampusTasks{ From 0f02a98f6699b769d281dbceba079a0609dc8a9e Mon Sep 17 00:00:00 2001 From: chenyifan01 Date: Fri, 16 Sep 2022 09:37:24 +0800 Subject: [PATCH 178/283] #2872 fix bug --- routers/repo/cloudbrain.go | 6 +++++- routers/repo/grampus.go | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index c521626cb..27670f23c 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -1841,6 +1841,11 @@ func SyncCloudbrainStatus() { task.Status = grampus.TransTrainJobStatus(result.JobInfo.Status) task.Duration = result.JobInfo.RunSec + if task.Duration < 0 { + task.Duration = 0 + } + task.TrainJobDuration = models.ConvertDurationToStr(task.Duration) + if task.StartTime == 0 && result.JobInfo.StartedAt > 0 { task.StartTime = timeutil.TimeStamp(result.JobInfo.StartedAt) } @@ -1848,7 +1853,6 @@ func SyncCloudbrainStatus() { task.EndTime = task.StartTime.Add(task.Duration) } task.CorrectCreateUnix() - task.ComputeAndSetDuration() if oldStatus != task.Status { notification.NotifyChangeCloudbrainStatus(task, oldStatus) } diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index a677f08a4..6ba25137d 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -709,6 +709,10 @@ func GrampusTrainJobShow(ctx *context.Context) { task.Status = grampus.TransTrainJobStatus(result.JobInfo.Status) if task.Status != result.JobInfo.Status || result.JobInfo.Status == models.GrampusStatusRunning { task.Duration = result.JobInfo.RunSec + if task.Duration < 0 { + task.Duration = 0 + } + task.TrainJobDuration = models.ConvertDurationToStr(task.Duration) if task.StartTime == 0 && result.JobInfo.StartedAt > 0 { task.StartTime = timeutil.TimeStamp(result.JobInfo.StartedAt) @@ -717,7 +721,6 @@ func GrampusTrainJobShow(ctx *context.Context) { task.EndTime = task.StartTime.Add(task.Duration) } task.CorrectCreateUnix() - task.ComputeAndSetDuration() if oldStatus != task.Status { notification.NotifyChangeCloudbrainStatus(task, oldStatus) } From 3d7ff1b6063ffc59ab87860fff6a68c1ace7d750 Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Fri, 16 Sep 2022 10:11:25 +0800 Subject: [PATCH 179/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routers/repo/grampus.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index 97beb638f..a770406d3 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -988,11 +988,11 @@ func generateCommand(repoName, processorType, codeRemotePath, dataRemotePath, bo command += "pwd;cd " + workDir + fmt.Sprintf(grampus.CommandPrepareScript, setting.Grampus.SyncScriptProject, setting.Grampus.SyncScriptProject) //download code & dataset if processorType == grampus.ProcessorTypeNPU { - commandDownload := "./downloader_for_obs " + setting.Bucket + " " + codeRemotePath + " " + grampus.CodeArchiveName + " " + dataRemotePath + " '" + datasetName + "'" + commandDownload := "./downloader_for_obs " + setting.Bucket + " " + codeRemotePath + " " + grampus.CodeArchiveName + " '" + dataRemotePath + "' '" + datasetName + "'" commandDownload = processPretrainModelParameter(pretrainModelPath, pretrainModelFileName, commandDownload) command += commandDownload } else if processorType == grampus.ProcessorTypeGPU { - commandDownload := "./downloader_for_minio " + setting.Grampus.Env + " " + codeRemotePath + " " + grampus.CodeArchiveName + " " + dataRemotePath + " '" + datasetName + "'" + commandDownload := "./downloader_for_minio " + setting.Grampus.Env + " " + codeRemotePath + " " + grampus.CodeArchiveName + " '" + dataRemotePath + "' '" + datasetName + "'" commandDownload = processPretrainModelParameter(pretrainModelPath, pretrainModelFileName, commandDownload) command += commandDownload } From 0fb745427b958e62003546b44dabaed4d5b3f405 Mon Sep 17 00:00:00 2001 From: zouap Date: Fri, 16 Sep 2022 11:18:18 +0800 Subject: [PATCH 180/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: zouap --- models/ai_model_manage.go | 16 +++++++++ routers/repo/ai_model_manage.go | 58 ++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/models/ai_model_manage.go b/models/ai_model_manage.go index 0ea01d6e5..97cae95a0 100644 --- a/models/ai_model_manage.go +++ b/models/ai_model_manage.go @@ -286,6 +286,22 @@ func ModifyModelDescription(id string, description string) error { return nil } +func ModifyModelStatus(id string, modelSize int64, status int, modelPath string) error { + var sess *xorm.Session + sess = x.ID(id) + defer sess.Close() + re, err := sess.Cols("size", "status", "path").Update(&AiModelManage{ + Size: modelSize, + Status: status, + Path: modelPath, + }) + if err != nil { + return err + } + log.Info("success to update ModelStatus from db.re=" + fmt.Sprint((re))) + return nil +} + func ModifyModelNewProperty(id string, new int, versioncount int) error { var sess *xorm.Session sess = x.ID(id) diff --git a/routers/repo/ai_model_manage.go b/routers/repo/ai_model_manage.go index d01539a75..1b295660a 100644 --- a/routers/repo/ai_model_manage.go +++ b/routers/repo/ai_model_manage.go @@ -27,6 +27,9 @@ const ( MODEL_LATEST = 1 MODEL_NOT_LATEST = 0 MODEL_MAX_SIZE = 1024 * 1024 * 1024 + STATUS_COPY_MODEL = 1 + STATUS_FINISHED = 0 + STATUS_ERROR = 2 ) func saveModelByParameters(jobId string, versionName string, name string, version string, label string, description string, engine int, ctx *context.Context) error { @@ -62,13 +65,9 @@ func saveModelByParameters(jobId string, versionName string, name string, versio modelSelectedFile := ctx.Query("modelSelectedFile") //download model zip //train type if aiTask.ComputeResource == models.NPUResource { - modelPath, modelSize, err = downloadModelFromCloudBrainTwo(id, aiTask.JobName, "", aiTask.TrainUrl, modelSelectedFile) - if err != nil { - log.Info("download model from CloudBrainTwo faild." + err.Error()) - return err - } cloudType = models.TypeCloudBrainTwo } else if aiTask.ComputeResource == models.GPUResource { + cloudType = models.TypeCloudBrainOne var ResourceSpecs *models.ResourceSpecs json.Unmarshal([]byte(setting.ResourceSpecs), &ResourceSpecs) for _, tmp := range ResourceSpecs.ResourceSpec { @@ -77,24 +76,8 @@ func saveModelByParameters(jobId string, versionName string, name string, versio aiTask.FlavorName = flaverName } } - modelPath, modelSize, err = downloadModelFromCloudBrainOne(id, aiTask.JobName, "", aiTask.TrainUrl, modelSelectedFile) - if err != nil { - log.Info("download model from CloudBrainOne faild." + err.Error()) - return err - } - cloudType = models.TypeCloudBrainOne } - // else if cloudType == models.TypeC2Net { - // if aiTask.ComputeResource == models.NPUResource { - // modelPath, modelSize, err = downloadModelFromCloudBrainTwo(id, aiTask.JobName, "", aiTask.TrainUrl, modelSelectedFile) - // if err != nil { - // log.Info("download model from CloudBrainTwo faild." + err.Error()) - // return err - // } - // } else if aiTask.ComputeResource == models.GPUResource { - - // } - // } + accuracy := make(map[string]string) accuracy["F1"] = "" accuracy["Recall"] = "" @@ -123,6 +106,7 @@ func saveModelByParameters(jobId string, versionName string, name string, versio Engine: int64(engine), TrainTaskInfo: string(aiTaskJson), Accuracy: string(accuracyJson), + Status: STATUS_COPY_MODEL, } err = models.SaveModelToDb(model) @@ -146,11 +130,41 @@ func saveModelByParameters(jobId string, versionName string, name string, versio models.UpdateRepositoryUnits(ctx.Repo.Repository, units, deleteUnitTypes) + go asyncToCopyModel(aiTask, id, modelSelectedFile) + log.Info("save model end.") notification.NotifyOtherTask(ctx.User, ctx.Repo.Repository, id, name, models.ActionCreateNewModelTask) return nil } +func asyncToCopyModel(aiTask *models.Cloudbrain, id string, modelSelectedFile string) { + if aiTask.ComputeResource == models.NPUResource { + modelPath, modelSize, err := downloadModelFromCloudBrainTwo(id, aiTask.JobName, "", aiTask.TrainUrl, modelSelectedFile) + if err != nil { + updateStatus(id, 0, STATUS_ERROR, modelPath) + log.Info("download model from CloudBrainTwo faild." + err.Error()) + } else { + updateStatus(id, modelSize, STATUS_FINISHED, modelPath) + } + } else if aiTask.ComputeResource == models.GPUResource { + + modelPath, modelSize, err := downloadModelFromCloudBrainOne(id, aiTask.JobName, "", aiTask.TrainUrl, modelSelectedFile) + if err != nil { + updateStatus(id, 0, STATUS_ERROR, modelPath) + log.Info("download model from CloudBrainOne faild." + err.Error()) + } else { + updateStatus(id, modelSize, STATUS_FINISHED, modelPath) + } + } +} + +func updateStatus(id string, modelSize int64, status int, modelPath string) { + err := models.ModifyModelStatus(id, modelSize, STATUS_FINISHED, modelPath) + if err != nil { + log.Info("update status error." + err.Error()) + } +} + func SaveNewNameModel(ctx *context.Context) { if !ctx.Repo.CanWrite(models.UnitTypeModelManage) { ctx.Error(403, ctx.Tr("repo.model_noright")) From 531ca27fe2e31169544e1c91aaaf53576b374ddb Mon Sep 17 00:00:00 2001 From: ychao_1983 Date: Fri, 16 Sep 2022 11:43:33 +0800 Subject: [PATCH 181/283] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/grampus/grampus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/grampus/grampus.go b/modules/grampus/grampus.go index 83fc3b1d4..a07aa49f8 100755 --- a/modules/grampus/grampus.go +++ b/modules/grampus/grampus.go @@ -32,7 +32,7 @@ var ( SpecialPools *models.SpecialPools - CommandPrepareScript = ";mkdir -p output;mkdir -p code;mkdir -p dataset;mkdir -p pretrainmodel;echo \"start loading script\";wget -q https://git.openi.org.cn/OpenIOSSG/%s/archive/master.zip;" + + CommandPrepareScript = ";mkdir -p output;mkdir -p code;mkdir -p dataset;mkdir -p pretrainmodel;echo \"start loading script\";wget https://git.openi.org.cn/OpenIOSSG/%s/archive/master.zip;" + "echo \"finish loading script\";unzip -q master.zip;cd %s;chmod 777 downloader_for_obs uploader_for_npu downloader_for_minio uploader_for_gpu;" ) From 2bebc01193058ff330318b27518683c8dcb519e2 Mon Sep 17 00:00:00 2001 From: liuzx Date: Fri, 16 Sep 2022 15:51:25 +0800 Subject: [PATCH 182/283] fix-bug --- routers/repo/cloudbrain.go | 7 ++++ routers/repo/grampus.go | 73 ++++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index b8d421ac6..ad4cc8130 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -157,6 +157,7 @@ func cloudBrainVersionNewDataPrepare(ctx *context.Context) error { log.Info("spec_id = %d", spec.ID) ctx.Data["spec_id"] = spec.ID } + prepareCloudbrainOneSpecs(ctx) var Parameters modelarts.Parameters if err := json.Unmarshal([]byte(ctx.Cloudbrain.Parameters), &Parameters); err != nil { ctx.ServerError("json.Unmarshal failed:", err) @@ -248,6 +249,12 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { tpl = tplCloudBrainTrainJobNew } + var jobID = ctx.Params(":jobid") + log.Info("jobIDTest= %s", jobID) + // if jobID != "" { + // tpl = tplCloudBrainTrainJobVersionNew + // } + tasks, err := models.GetCloudbrainsByDisplayJobName(repo.ID, jobType, displayJobName) if err == nil { if len(tasks) != 0 { diff --git a/routers/repo/grampus.go b/routers/repo/grampus.go index 97beb638f..0e8b457bd 100755 --- a/routers/repo/grampus.go +++ b/routers/repo/grampus.go @@ -343,10 +343,16 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain codeMinioPath := setting.CBCodePathPrefix + jobName + cloudbrain.CodeMountPath + "/" branchName := form.BranchName image := strings.TrimSpace(form.Image) + tpl := tplGrampusTrainJobGPUNew + + var jobID = ctx.Params(":jobid") + if jobID != "" { + tpl = tplGrampusTrainJobGPUVersionNew + } if !jobNamePattern.MatchString(displayJobName) { grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_jobname_err"), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_jobname_err"), tpl, &form) return } @@ -354,14 +360,14 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil || !bootFileExist { log.Error("Get bootfile error:", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_bootfile_err"), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_bootfile_err"), tpl, &form) return } errStr := checkSpecialPool(ctx, "GPU") if errStr != "" { grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(errStr, tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(errStr, tpl, &form) return } @@ -370,13 +376,13 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("GetGrampusCountByUserID failed:%v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr("system error", tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr("system error", tpl, &form) return } else { if count >= 1 { log.Error("the user already has running or waiting task", ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr("you have already a running or waiting task, can not create more", tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr("you have already a running or waiting task, can not create more", tpl, &form) return } } @@ -385,7 +391,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := grampusParamCheckCreateTrainJob(form); err != nil { log.Error("paramCheckCreateTrainJob failed:(%v)", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(err.Error(), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(err.Error(), tpl, &form) return } @@ -395,14 +401,14 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if len(tasks) != 0 { log.Error("the job name did already exist", ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr("the job name did already exist", tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr("the job name did already exist", tpl, &form) return } } else { if !models.IsErrJobNotExist(err) { log.Error("system error, %v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr("system error", tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr("system error", tpl, &form) return } } @@ -415,7 +421,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain }) if err != nil || spec == nil { grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr("Resource specification not available", tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr("Resource specification not available", tpl, &form) return } @@ -425,7 +431,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("GetDatasetInfo failed: %v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.error.dataset_select"), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.error.dataset_select"), tpl, &form) return } @@ -438,7 +444,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := downloadZipCode(ctx, codeLocalPath, branchName); err != nil { log.Error("downloadZipCode failed, server timed out: %s (%v)", repo.FullName(), err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tpl, &form) return } @@ -447,7 +453,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := uploadCodeToMinio(codeLocalPath+"/", jobName, cloudbrain.CodeMountPath+"/"); err != nil { log.Error("Failed to uploadCodeToMinio: %s (%v)", repo.FullName(), err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tpl, &form) return } @@ -455,7 +461,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := mkModelPath(modelPath); err != nil { log.Error("Failed to mkModelPath: %s (%v)", repo.FullName(), err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tpl, &form) return } @@ -463,7 +469,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := uploadCodeToMinio(modelPath, jobName, cloudbrain.ModelMountPath+"/"); err != nil { log.Error("Failed to uploadCodeToMinio: %s (%v)", repo.FullName(), err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tpl, &form) return } @@ -486,7 +492,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("Failed to generateCommand: %s (%v)", displayJobName, err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr("Create task failed, internal error", tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr("Create task failed, internal error", tpl, &form) return } @@ -528,7 +534,7 @@ func GrampusTrainJobGpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("GenerateTrainJob failed:%v", err.Error(), ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeGPU) - ctx.RenderWithErr(err.Error(), tplGrampusTrainJobGPUNew, &form) + ctx.RenderWithErr(err.Error(), tpl, &form) return } ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/modelarts/train-job") @@ -595,10 +601,17 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain isLatestVersion := modelarts.IsLatestVersion versionCount := modelarts.VersionCountOne engineName := form.EngineName + tpl := tplGrampusTrainJobNPUNew + + //判断路由是否存在jobID,若存在,则说明是创建版本 + var jobID = ctx.Params(":jobid") + if jobID != "" { + tpl = tplGrampusTrainJobNPUVersionNew + } if !jobNamePattern.MatchString(displayJobName) { grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_jobname_err"), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_jobname_err"), tpl, &form) return } @@ -606,7 +619,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil || !bootFileExist { log.Error("Get bootfile error:", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_bootfile_err"), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(ctx.Tr("repo.cloudbrain_bootfile_err"), tpl, &form) return } @@ -622,13 +635,13 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("GetGrampusCountByUserID failed:%v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr("system error", tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr("system error", tpl, &form) return } else { if count >= 1 { log.Error("the user already has running or waiting task", ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr("you have already a running or waiting task, can not create more", tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr("you have already a running or waiting task, can not create more", tpl, &form) return } } @@ -637,7 +650,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := grampusParamCheckCreateTrainJob(form); err != nil { log.Error("paramCheckCreateTrainJob failed:(%v)", err) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(err.Error(), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(err.Error(), tpl, &form) return } @@ -647,14 +660,14 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if len(tasks) != 0 { log.Error("the job name did already exist", ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr("the job name did already exist", tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr("the job name did already exist", tpl, &form) return } } else { if !models.IsErrJobNotExist(err) { log.Error("system error, %v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr("system error", tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr("system error", tpl, &form) return } } @@ -667,7 +680,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain }) if err != nil || spec == nil { grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr("Resource specification not available", tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr("Resource specification not available", tpl, &form) return } @@ -676,7 +689,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("GetDatasetInfo failed: %v", err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.error.dataset_select"), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.error.dataset_select"), tpl, &form) return } @@ -689,7 +702,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := downloadZipCode(ctx, codeLocalPath, branchName); err != nil { log.Error("downloadZipCode failed, server timed out: %s (%v)", repo.FullName(), err) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tpl, &form) return } @@ -697,14 +710,14 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err := obsMkdir(setting.CodePathPrefix + jobName + modelarts.OutputPath); err != nil { log.Error("Failed to obsMkdir_output: %s (%v)", repo.FullName(), err) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tpl, &form) return } if err := uploadCodeToObs(codeLocalPath, jobName, ""); err != nil { log.Error("Failed to uploadCodeToObs: %s (%v)", repo.FullName(), err) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(ctx.Tr("cloudbrain.load_code_failed"), tpl, &form) return } @@ -726,7 +739,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("Failed to generateCommand: %s (%v)", displayJobName, err, ctx.Data["MsgID"]) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr("Create task failed, internal error", tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr("Create task failed, internal error", tpl, &form) return } @@ -769,7 +782,7 @@ func GrampusTrainJobNpuCreate(ctx *context.Context, form auth.CreateGrampusTrain if err != nil { log.Error("GenerateTrainJob failed:%v", err.Error()) grampusTrainJobNewDataPrepare(ctx, grampus.ProcessorTypeNPU) - ctx.RenderWithErr(err.Error(), tplGrampusTrainJobNPUNew, &form) + ctx.RenderWithErr(err.Error(), tpl, &form) return } ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/modelarts/train-job") From 928a7b8d215cc76664e12cd2c291bf654632aa2c Mon Sep 17 00:00:00 2001 From: zhoupzh Date: Fri, 16 Sep 2022 16:07:39 +0800 Subject: [PATCH 183/283] fix issue --- templates/repo/modelmanage/index.tmpl | 4 +- web_src/js/components/Model.vue | 57 +++++++++++++++++++++------ web_src/js/features/i18nVue.js | 8 ++++ 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/templates/repo/modelmanage/index.tmpl b/templates/repo/modelmanage/index.tmpl index 3a5240768..b2994f0c2 100644 --- a/templates/repo/modelmanage/index.tmpl +++ b/templates/repo/modelmanage/index.tmpl @@ -46,9 +46,9 @@
{{template "repo/header" .}} -
+
{{template "base/alert" .}} -
+