mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
裁剪
swagger 移除 merchant swagger 改造首页内容为通用后台管理系统定位 - 修改首页标题从 'LINUX DO Credit' 改为 'Modern Platform' - 更新副标题为 '为二次开发而生' - 更新首页描述为通用平台的特点 - 更新首页特性标签为 '开箱即用、高度可扩展、工业级基建' - 修改展示卡片为技术栈和二次开发相关 - 更新开发者示例代码为通用的注册和 API Key 获取示例 - 更新页脚品牌名为 'Modern Platform' - 调整页脚导航链接为通用平台相关内容 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 裁剪 移除 /api/v1/user/pay-key 相关代码 - 删除后端 UpdatePayKey 处理器函数和 UpdatePayKeyRequest 结构体 - 删除 User 模型中的 PayKey 字段 - 删除 User.VerifyPayKey 方法 - 删除 EncryptPayKeyFailed 错误常量 - 删除 /api/v1/user/pay-key PUT 路由 - 删除 OAuth 返回中的 IsPayKey 字段 - 删除前端 UserService.updatePayKey 方法 - 删除前端所有支付密钥 UI 和逻辑 - 更新相关的导出和注释 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 api 修正 系统配置 前端裁剪 后端裁剪 init
This commit is contained in:
@@ -17,5 +17,4 @@ limitations under the License.
|
||||
package user
|
||||
|
||||
const (
|
||||
EncryptPayKeyFailed = "加密支付密码失败"
|
||||
)
|
||||
|
||||
@@ -15,51 +15,3 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/apps/oauth"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
)
|
||||
|
||||
// UpdatePayKeyRequest 更新支付密钥请求
|
||||
type UpdatePayKeyRequest struct {
|
||||
PayKey string `json:"pay_key" binding:"required,max=6"`
|
||||
}
|
||||
|
||||
// UpdatePayKey 更新用户支付密钥
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body UpdatePayKeyRequest true "request body"
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Router /api/v1/user/pay-key [put]
|
||||
func UpdatePayKey(c *gin.Context) {
|
||||
var req UpdatePayKeyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
user, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
|
||||
encryptedPayKey, err := util.Encrypt(user.SignKey, req.PayKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(EncryptPayKeyFailed))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(c.Request.Context()).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", user.ID).
|
||||
Update("pay_key", encryptedPayKey).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
}
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis_rate/v10"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/linux-do/credit/internal/common"
|
||||
"github.com/linux-do/credit/internal/config"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/logger"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/task"
|
||||
"github.com/linux-do/credit/internal/task/scheduler"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
linuxDoRateLimiter *redis_rate.Limiter
|
||||
rateLimitKey string
|
||||
)
|
||||
|
||||
func init() {
|
||||
linuxDoRateLimiter = redis_rate.NewLimiter(db.Redis)
|
||||
rateLimitKey = db.PrefixedKey(linuxDoAPIRateLimitKey)
|
||||
}
|
||||
|
||||
// waitForRateLimit 等待获取限流令牌(阻塞直到获取到令牌)
|
||||
func waitForRateLimit(ctx context.Context, key string, limit redis_rate.Limit) error {
|
||||
for {
|
||||
res, err := linuxDoRateLimiter.Allow(ctx, key, limit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redis 限流器错误: %w", err)
|
||||
}
|
||||
if res.Allowed > 0 {
|
||||
return nil // 获取到令牌
|
||||
}
|
||||
// 未获取到令牌,等待后重试
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(res.RetryAfter):
|
||||
// 继续重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleUpdateUserGamificationScores 处理所有用户积分更新任务
|
||||
func HandleUpdateUserGamificationScores(ctx context.Context, t *asynq.Task) error {
|
||||
rateLimit := config.Config.Worker.GamificationScoreRateLimit
|
||||
limit := redis_rate.Limit{
|
||||
Rate: rateLimit.Rate,
|
||||
Burst: rateLimit.Rate,
|
||||
Period: time.Duration(rateLimit.Period) * time.Second,
|
||||
}
|
||||
|
||||
page := 0
|
||||
totalProcessed := 0
|
||||
|
||||
for {
|
||||
if err := waitForRateLimit(ctx, rateLimitKey, limit); err != nil {
|
||||
logger.ErrorF(ctx, "速率限制等待失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
leaderboard, err := model.GetLeaderboard(ctx, page)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "获取排行榜第 %d 页失败: %v", page, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(leaderboard.Users) == 0 {
|
||||
logger.InfoF(ctx, "[调度] 排行榜数据处理完成,共处理 %d 个用户", totalProcessed)
|
||||
break
|
||||
}
|
||||
|
||||
if err = enqueueBatchScoreTask(ctx, leaderboard.Users); err != nil {
|
||||
logger.ErrorF(ctx, "下发第 %d 页批量任务失败: %v", page, err)
|
||||
return err
|
||||
}
|
||||
|
||||
totalProcessed += len(leaderboard.Users)
|
||||
logger.InfoF(ctx, "[调度] 已处理排行榜第 %d 页,本页 %d 个用户,累计 %d 个用户",
|
||||
page, len(leaderboard.Users), totalProcessed)
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// enqueueBatchScoreTask 下发批量积分更新任务
|
||||
func enqueueBatchScoreTask(ctx context.Context, userScores []model.LeaderboardUser) error {
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"user_scores": userScores,
|
||||
})
|
||||
|
||||
opts := []asynq.Option{
|
||||
asynq.Queue(task.QueueWhitelistOnly),
|
||||
asynq.MaxRetry(5),
|
||||
}
|
||||
|
||||
if _, err := scheduler.AsynqClient.Enqueue(asynq.NewTask(task.UpdateSingleUserGamificationScoreTask, payload), opts...); err != nil {
|
||||
logger.ErrorF(ctx, "下发批量积分任务失败: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// batchUpdateUserScores 批量更新用户积分
|
||||
func batchUpdateUserScores(ctx context.Context, userScores []model.LeaderboardUser) error {
|
||||
if len(userScores) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
userIDs := make([]uint64, len(userScores))
|
||||
scoreMap := make(map[uint64]int64, len(userScores))
|
||||
for i, u := range userScores {
|
||||
userIDs[i] = u.ID
|
||||
scoreMap[u.ID] = u.TotalScore
|
||||
}
|
||||
|
||||
users, err := model.GetByIDs(db.DB(ctx), userIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("批量查询用户失败: %w", err)
|
||||
}
|
||||
|
||||
protectionDays, err := model.GetIntByKey(ctx, model.ConfigKeyNewUserProtectionDays)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", common.GetProtectionDaysFailed, err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
return db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, user := range users {
|
||||
newScore, exists := scoreMap[user.ID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
newCommunityBalance := decimal.NewFromInt(newScore)
|
||||
oldCommunityBalance := user.CommunityBalance
|
||||
diff := newCommunityBalance.Sub(oldCommunityBalance)
|
||||
|
||||
createOrder := func(amount decimal.Decimal, remark string) error {
|
||||
order := model.Order{
|
||||
OrderName: "社区积分更新",
|
||||
PayerUserID: 0,
|
||||
PayeeUserID: user.ID,
|
||||
Amount: amount,
|
||||
Status: model.OrderStatusSuccess,
|
||||
Type: model.OrderTypeCommunity,
|
||||
Remark: remark,
|
||||
TradeTime: now,
|
||||
ExpiresAt: now,
|
||||
}
|
||||
if err = tx.Create(&order).Error; err != nil {
|
||||
return fmt.Errorf("创建用户[%s]订单失败: %w", user.Username, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if user.CommunityBalance.IsZero() && user.TotalCommunity.IsZero() {
|
||||
if err = tx.Model(&user).UpdateColumns(map[string]interface{}{
|
||||
"community_balance": newCommunityBalance,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("初始化用户[%s]社区积分失败: %w", user.Username, err)
|
||||
}
|
||||
logger.InfoF(ctx, "用户[%s]首次同步社区积分: %s", user.Username, newCommunityBalance.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// 积分未变化
|
||||
if diff.IsZero() {
|
||||
remark := fmt.Sprintf("社区积分从 %s 更新到 %s,变化 %s", oldCommunityBalance.String(), newCommunityBalance.String(), diff.String())
|
||||
if err = createOrder(decimal.Zero, remark); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 新用户保护期检查
|
||||
if diff.IsNegative() && protectionDays > 0 {
|
||||
registeredDays := int(time.Since(user.CreatedAt).Hours() / 24)
|
||||
if registeredDays < protectionDays {
|
||||
if err = tx.Model(&user).UpdateColumns(map[string]interface{}{
|
||||
"community_balance": newCommunityBalance,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("更新用户[%s]积分失败: %w", user.Username, err)
|
||||
}
|
||||
remark := fmt.Sprintf("社区积分从 %s 更新到 %s,变化 %s(保护期内,跳过扣分)",
|
||||
oldCommunityBalance.String(), newCommunityBalance.String(), diff.String())
|
||||
if err = createOrder(decimal.Zero, remark); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.InfoF(ctx, "用户[%s]在保护期内,积分下降%s,跳过扣分", user.Username, diff.Abs().String())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户积分
|
||||
if err = tx.Model(&user).UpdateColumns(map[string]interface{}{
|
||||
"community_balance": newCommunityBalance,
|
||||
"total_community": gorm.Expr("total_community + ?", diff),
|
||||
"total_receive": gorm.Expr("total_receive + ?", diff),
|
||||
"available_balance": gorm.Expr("available_balance + ?", diff),
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("更新用户[%s]积分失败: %w", user.Username, err)
|
||||
}
|
||||
|
||||
remark := fmt.Sprintf("社区积分从 %s 更新到 %s,变化 %s",
|
||||
oldCommunityBalance.String(), newCommunityBalance.String(), diff.String())
|
||||
if err = createOrder(diff, remark); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// HandleUpdateSingleUserGamificationScore 处理用户积分更新任务
|
||||
func HandleUpdateSingleUserGamificationScore(ctx context.Context, t *asynq.Task) error {
|
||||
var payload struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
UserScores []model.LeaderboardUser `json:"user_scores"`
|
||||
}
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
return fmt.Errorf("解析任务参数失败: %w", err)
|
||||
}
|
||||
|
||||
if len(payload.UserScores) > 0 {
|
||||
return batchUpdateUserScores(ctx, payload.UserScores)
|
||||
}
|
||||
|
||||
if payload.UserID > 0 {
|
||||
var user model.User
|
||||
if err := user.GetByID(db.DB(ctx), payload.UserID); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.InfoF(ctx, "用户[%d]不存在,跳过积分更新", payload.UserID)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("查询用户[%d]失败: %w", payload.UserID, err)
|
||||
}
|
||||
|
||||
response, errGet := user.GetUserGamificationScore(ctx)
|
||||
if errGet != nil {
|
||||
logger.ErrorF(ctx, "处理用户[%s]失败: %v", user.Username, errGet)
|
||||
return errGet
|
||||
}
|
||||
|
||||
return batchUpdateUserScores(ctx, []model.LeaderboardUser{
|
||||
{ID: payload.UserID, TotalScore: response.User.GamificationScore},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user