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:
ryan
2026-06-07 15:32:14 +08:00
parent 8a782525de
commit 48d414e197
180 changed files with 1538 additions and 27463 deletions
-1
View File
@@ -17,5 +17,4 @@ limitations under the License.
package user
const (
EncryptPayKeyFailed = "加密支付密码失败"
)
-48
View File
@@ -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())
}
-279
View File
@@ -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
}