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
-52
View File
@@ -1,52 +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 model
import (
"time"
"github.com/linux-do/credit/internal/db/idgen"
"gorm.io/gorm"
)
type DisputeStatus string
const (
DisputeStatusDisputing DisputeStatus = "disputing"
DisputeStatusRefund DisputeStatus = "refund"
DisputeStatusClosed DisputeStatus = "closed"
)
type Dispute struct {
ID uint64 `json:"id,string" gorm:"primaryKey"`
OrderID uint64 `json:"order_id,string" gorm:"uniqueIndex:idx_dispute_order;index:idx_dispute_order_status,priority:1;not null"`
InitiatorUserID uint64 `json:"initiator_user_id" gorm:"not null;index:idx_initiator_status_created,priority:1"`
Reason string `json:"reason" gorm:"size:500;not null"`
Status DisputeStatus `json:"status" gorm:"type:varchar(20);index;index:idx_dispute_order_status,priority:2;index:idx_initiator_status_created,priority:2;not null;default:'disputing'"`
HandlerUserID *uint64 `json:"handler_user_id" gorm:"index"`
InitiatorUsername string `json:"initiator_username" gorm:"-:migration;->"`
HandlerUsername string `json:"handler_username" gorm:"-:migration;->"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_initiator_status_created,priority:3"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
func (d *Dispute) BeforeCreate(*gorm.DB) error {
if d.ID == 0 {
d.ID = idgen.NextUint64ID()
}
return nil
}
-58
View File
@@ -1,58 +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 model
import (
"time"
"github.com/linux-do/credit/internal/db/idgen"
"gorm.io/gorm"
)
type MerchantAPIKey struct {
ID uint64 `json:"id,string" gorm:"primaryKey"`
UserID uint64 `json:"user_id" gorm:"not null;index:idx_merchant_api_keys_user_created,priority:1"`
ClientID string `json:"client_id" gorm:"size:64;uniqueIndex;index:idx_client_credentials,priority:2;not null"`
ClientSecret string `json:"client_secret" gorm:"size:64;index:idx_client_credentials,priority:1;not null"`
AppName string `json:"app_name" gorm:"size:20;not null"`
AppHomepageURL string `json:"app_homepage_url" gorm:"size:100;not null"`
AppDescription string `json:"app_description" gorm:"size:100"`
RedirectURI string `json:"redirect_uri" gorm:"size:100"`
NotifyURL string `json:"notify_url" gorm:"size:100;not null"`
PublicKey []byte `json:"public_key" gorm:"type:bytea"`
TestMode bool `json:"test_mode" gorm:"default:false"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_merchant_api_keys_user_created,priority:2"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
}
// GetByID 通过 ID 查询商户 API Key
func (m *MerchantAPIKey) GetByID(tx *gorm.DB, id uint64) error {
return tx.Where("id = ?", id).First(m).Error
}
// GetByClientID 通过 ClientID 查询商户 API Key
func (m *MerchantAPIKey) GetByClientID(tx *gorm.DB, clientID string) error {
return tx.Where("client_id = ?", clientID).First(m).Error
}
func (m *MerchantAPIKey) BeforeCreate(*gorm.DB) error {
if m.ID == 0 {
m.ID = idgen.NextUint64ID()
}
return nil
}
-51
View File
@@ -1,51 +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 model
import (
"time"
"github.com/linux-do/credit/internal/db/idgen"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type MerchantPaymentLink struct {
ID uint64 `json:"id,string" gorm:"primaryKey"`
MerchantAPIKeyID uint64 `json:"merchant_api_key_id,string" gorm:"not null;index"`
Token string `json:"token" gorm:"size:64;uniqueIndex;not null"`
Amount decimal.Decimal `json:"amount" gorm:"type:numeric(20,2);not null"`
ProductName string `json:"product_name" gorm:"size:30;not null"`
Remark string `json:"remark" gorm:"size:100"`
TotalLimit *uint `json:"total_limit" gorm:"default:null"`
UserLimit *uint `json:"user_limit" gorm:"default:null"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
}
// GetByToken 通过 Token 查询支付链接
func (m *MerchantPaymentLink) GetByToken(tx *gorm.DB, token string) error {
return tx.Where("token = ?", token).First(m).Error
}
func (m *MerchantPaymentLink) BeforeCreate(*gorm.DB) error {
if m.ID == 0 {
m.ID = idgen.NextUint64ID()
}
return nil
}
-55
View File
@@ -1,55 +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 model
import (
"context"
"time"
"github.com/linux-do/credit/internal/db"
"github.com/shopspring/decimal"
)
type OrderTransferStatus string
const (
OrderTransferStatusPending OrderTransferStatus = "pending"
OrderTransferStatusCompleted OrderTransferStatus = "completed"
)
type OrderTransfer struct {
ID uint64 `json:"id,string" gorm:"primaryKey;autoIncrement"`
OrderID uint64 `json:"order_id,string" gorm:"index;uniqueIndex:uk_order_id;index:idx_order_status,priority:1"`
PayeeUserID uint64 `json:"payee_user_id" gorm:"index"`
Amount decimal.Decimal `json:"amount" gorm:"type:numeric(20,2);not null"`
Status OrderTransferStatus `json:"status" gorm:"type:varchar(20);not null;index:idx_status_transfer_at,priority:1;index:idx_order_status,priority:2"`
TransferAt time.Time `json:"transfer_at" gorm:"not null;index:idx_status_transfer_at,priority:2"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
}
func GetDueTransferOrders(ctx context.Context, limit int) ([]OrderTransfer, error) {
var transfers []OrderTransfer
if err := db.DB(ctx).
Where("status = ? AND transfer_at <= ?", OrderTransferStatusPending, time.Now()).
Order("status, transfer_at ASC").
Limit(limit).
Find(&transfers).Error; err != nil {
return nil, err
}
return transfers, nil
}
-107
View File
@@ -1,107 +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 model
import (
"context"
"fmt"
"time"
"github.com/linux-do/credit/internal/db"
"github.com/linux-do/credit/internal/db/idgen"
"github.com/linux-do/credit/internal/logger"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type OrderType string
const (
OrderTypeReceive OrderType = "receive"
OrderTypePayment OrderType = "payment"
OrderTypeTransfer OrderType = "transfer"
OrderTypeCommunity OrderType = "community"
OrderTypeOnline OrderType = "online"
OrderTypeTest OrderType = "test"
OrderTypeDistribute OrderType = "distribute"
OrderTypeRedEnvelopeSend OrderType = "red_envelope_send"
OrderTypeRedEnvelopeReceive OrderType = "red_envelope_receive"
OrderTypeRedEnvelopeRefund OrderType = "red_envelope_refund"
)
type OrderStatus string
const (
OrderStatusSuccess OrderStatus = "success"
OrderStatusFailed OrderStatus = "failed"
OrderStatusPending OrderStatus = "pending"
OrderStatusExpired OrderStatus = "expired"
OrderStatusDisputing OrderStatus = "disputing"
OrderStatusRefund OrderStatus = "refund"
OrderStatusRefused OrderStatus = "refused"
)
type Order struct {
ID uint64 `json:"id,string" gorm:"primaryKey"`
OrderNo string `json:"order_no" gorm:"-"`
OrderName string `json:"order_name" gorm:"size:64;not null;index"`
MerchantOrderNo *string `json:"merchant_order_no" gorm:"size:64;uniqueIndex:idx_orders_client_merchant_order,priority:2"`
ClientID string `json:"client_id" gorm:"size:64;index:idx_orders_client_status_created,priority:1;index:idx_orders_client_payee,priority:1;index:idx_orders_client_payer,priority:1;uniqueIndex:idx_orders_client_merchant_order,priority:1"`
PayerUserID uint64 `json:"payer_user_id" gorm:"index:idx_orders_payer_status_type_created,priority:1;index:idx_orders_payer_status_type_trade,priority:1;index:idx_orders_client_payer,priority:2"`
PayeeUserID uint64 `json:"payee_user_id" gorm:"index:idx_orders_payee_status_type_created,priority:1;index:idx_orders_client_payee,priority:2"`
PayerUsername string `json:"payer_username" gorm:"-:migration;->"`
PayeeUsername string `json:"payee_username" gorm:"-:migration;->"`
Amount decimal.Decimal `json:"amount" gorm:"type:numeric(20,2);not null;index"`
Status OrderStatus `json:"status" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:2;index:idx_orders_payer_status_type_created,priority:2;index:idx_orders_client_status_created,priority:2;index:idx_orders_payer_status_type_trade,priority:2;index:idx_orders_payment_link_status,priority:2;index:idx_orders_status_expires,priority:1"`
Type OrderType `json:"type" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:3;index:idx_orders_payer_status_type_created,priority:3;index:idx_orders_payer_status_type_trade,priority:3"`
Remark string `json:"remark" gorm:"size:255"`
PaymentType string `json:"payment_type" gorm:"size:20"`
RedirectURI string `json:"redirect_uri" gorm:"size:100"`
NotifyURL string `json:"notify_url" gorm:"size:100"`
PaymentLinkID *uint64 `json:"payment_link_id,string" gorm:"index:idx_orders_payment_link_status,priority:1"`
TradeTime time.Time `json:"trade_time" gorm:"index:idx_orders_payer_status_type_trade,priority:4"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;index:idx_orders_status_expires,priority:2"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_orders_payee_status_type_created,priority:4;index:idx_orders_payer_status_type_created,priority:4;index:idx_orders_client_status_created,priority:3"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
}
func (o *Order) BeforeCreate(*gorm.DB) error {
if o.ID == 0 {
o.ID = idgen.NextUint64ID()
}
return nil
}
// AfterFind 格式化 OrderNo
func (o *Order) AfterFind(*gorm.DB) error {
o.OrderNo = fmt.Sprintf("%018d", o.ID)
return nil
}
// ExpirePendingOrders 将已过期且 pending 状态的订单设置为 expired
func ExpirePendingOrders(ctx context.Context) {
result := db.DB(ctx).Model(&Order{}).
Where("status = ? AND expires_at <= ?", OrderStatusPending, time.Now()).
Update("status", OrderStatusExpired)
if result.Error != nil {
logger.ErrorF(ctx, "过期 pending 订单失败: %v", result.Error)
} else {
logger.InfoF(ctx, "已将 %d 个已过期的 pending 订单设置为 expired", result.RowsAffected)
}
}
-69
View File
@@ -1,69 +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 model
import (
"time"
"github.com/shopspring/decimal"
)
type RedEnvelopeType string
const (
RedEnvelopeTypeFixed RedEnvelopeType = "fixed"
RedEnvelopeTypeRandom RedEnvelopeType = "random"
)
type RedEnvelopeStatus string
const (
RedEnvelopeStatusActive RedEnvelopeStatus = "active"
RedEnvelopeStatusFinished RedEnvelopeStatus = "finished"
RedEnvelopeStatusExpired RedEnvelopeStatus = "expired"
)
// RedEnvelope 红包
type RedEnvelope struct {
ID uint64 `json:"id,string" gorm:"primaryKey"`
CreatorID uint64 `json:"creator_id,string" gorm:"index;not null"`
CreatorUsername string `json:"creator_username" gorm:"-:migration;->"`
CreatorAvatarURL string `json:"creator_avatar_url" gorm:"-:migration;->"`
Type RedEnvelopeType `json:"type" gorm:"type:varchar(20);not null"`
TotalAmount decimal.Decimal `json:"total_amount" gorm:"type:numeric(20,2);not null"`
RemainingAmount decimal.Decimal `json:"remaining_amount" gorm:"type:numeric(20,2);not null"`
TotalCount int `json:"total_count" gorm:"not null"`
RemainingCount int `json:"remaining_count" gorm:"not null"`
Greeting string `json:"greeting" gorm:"size:100"`
Status RedEnvelopeStatus `json:"status" gorm:"type:varchar(20);not null"`
CoverUploadID *uint64 `json:"cover_upload_id,string,omitempty" gorm:"index"`
HeterotypicUploadID *uint64 `json:"heterotypic_upload_id,string,omitempty" gorm:"index"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
// RedEnvelopeClaim 红包领取记录
type RedEnvelopeClaim struct {
ID uint64 `json:"id,string" gorm:"primaryKey"`
RedEnvelopeID uint64 `json:"red_envelope_id,string" gorm:"uniqueIndex:idx_red_envelope_user,priority:2;not null"`
UserID uint64 `json:"user_id,string" gorm:"uniqueIndex:idx_red_envelope_user,priority:1;not null"`
Username string `json:"username" gorm:"-:migration;->"`
AvatarURL string `json:"avatar_url" gorm:"-:migration;->"`
Amount decimal.Decimal `json:"amount" gorm:"type:numeric(20,2);not null"`
ClaimedAt time.Time `json:"claimed_at" gorm:"autoCreateTime"`
}
+5 -43
View File
@@ -20,7 +20,6 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"strconv"
"time"
@@ -32,21 +31,10 @@ import (
// 配置键常量 - 所有系统配置的 key 定义
const (
ConfigKeyMerchantOrderExpireMinutes = "merchant_order_expire_minutes" // 商家订单过期时间(分钟)
ConfigKeyWebsiteOrderExpireMinutes = "website_order_expire_minutes" // 网站订单过期时间(分钟)
ConfigKeyDisputeTimeWindowHours = "dispute_time_window_hours" // 商家争议时间窗口(小时)
ConfigKeyNewUserInitialCredit = "new_user_initial_credit" // 新用户注册初始积分
ConfigKeyNewUserProtectionDays = "new_user_protection_days" // 新用户保护期天数(期内不扣分)
ConfigKeyLeaderboardCacheTTLSeconds = "leaderboard_cache_ttl_seconds" // 排行榜缓存过期时间(秒)
ConfigKeyRedEnvelopeEnabled = "red_envelope_enabled" // 红包功能是否启用(1启用,0禁用)
ConfigKeyRedEnvelopeMaxAmount = "red_envelope_max_amount" // 单个红包的最大积分上限
ConfigKeyRedEnvelopeDailyLimit = "red_envelope_daily_limit" // 每日发红包的个数限制
ConfigKeyRedEnvelopeFeeRate = "red_envelope_fee_rate" // 红包手续费率(0-1之间的小数,0表示不收费)
ConfigKeyRedEnvelopeMaxRecipients = "red_envelope_max_recipients" // 每个红包的最大可领取人数上限
ConfigKeyUserBalanceStatsCacheTTL = "user_balance_stats_cache_ttl" // 用户余额统计缓存过期时间(秒)
ConfigKeyUploadAllowedExtensions = "upload_allowed_extensions" // 允许上传的文件扩展名,逗号分隔
ConfigKeySettlementDelayDaysMin = "settlement_delay_days_min" // 商户收款延迟到账最小天数(0表示即时到账)
ConfigKeySettlementDelayDaysMax = "settlement_delay_days_max" // 商户收款延迟到账最大天数(实际天数在min~max随机)
ConfigKeyUploadAllowedExtensions = "upload_allowed_extensions" // 允许上传的文件扩展名,逗号分隔
ConfigKeySiteName = "site_name" // 站点名称
ConfigKeyRegistrationEnabled = "registration_enabled" // 是否允许注册
ConfigKeyMaxAPIKeysPerUser = "max_api_keys_per_user" // 每个用户最大 API Key 数量
)
const (
@@ -57,6 +45,7 @@ const (
type SystemConfig struct {
Key string `json:"key" gorm:"primaryKey;size:64;not null"`
Value string `json:"value" gorm:"size:255;not null"`
Type string `json:"type" gorm:"size:32;not null;default:'system'"`
Description string `json:"description" gorm:"size:255"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
@@ -128,30 +117,3 @@ func GetBoolByKey(ctx context.Context, key string) (bool, error) {
return value, nil
}
const defaultHoldDays = 7
func GetRandomHoldDays(ctx context.Context) int {
// get config
holdDays := defaultHoldDays
holdDaysMin, errMin := GetIntByKey(ctx, ConfigKeySettlementDelayDaysMin)
if errMin != nil || holdDaysMin <= 0 {
holdDaysMin = defaultHoldDays
}
holdDaysMax, errMax := GetIntByKey(ctx, ConfigKeySettlementDelayDaysMax)
if errMax != nil || holdDaysMax <= 0 {
holdDaysMax = defaultHoldDays
}
// check config
if holdDaysMin == holdDaysMax {
return holdDaysMin
}
if holdDaysMin > holdDaysMax {
return holdDays
}
return holdDaysMin + rand.Intn(holdDaysMax-holdDaysMin+1)
}
func GetRandomSettleAt(ctx context.Context) time.Time {
return time.Now().AddDate(0, 0, GetRandomHoldDays(ctx))
}
-58
View File
@@ -1,58 +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 model
import (
"time"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type PayLevel uint8
const (
PayLevelFree PayLevel = iota
PayLevelBasic
PayLevelStandard
PayLevelPremium
)
type UserPayConfig struct {
ID uint64 `json:"id,string" gorm:"primaryKey;autoIncrement"`
Level PayLevel `json:"level" gorm:"uniqueIndex;not null"`
MinScore int64 `json:"min_score" gorm:"not null;index:idx_score_range,priority:1"`
MaxScore *int64 `json:"max_score" gorm:"index:idx_score_range,priority:2"`
DailyLimit *int64 `json:"daily_limit"`
FeeRate decimal.Decimal `json:"fee_rate" gorm:"type:numeric(3,2);default:0;check:fee_rate >= 0 AND fee_rate <= 1"`
ScoreRate decimal.Decimal `json:"score_rate" gorm:"type:numeric(3,2);default:0;check:score_rate >= 0 AND score_rate <= 1"`
DistributeRate decimal.Decimal `json:"distribute_rate" gorm:"type:numeric(3,2);default:0;check:distribute_rate >= 0 AND distribute_rate <= 1"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
// GetByPayScore 通过 pay_score 查询对应的支付配置
func (upc *UserPayConfig) GetByPayScore(tx *gorm.DB, payScore int64) error {
return tx.Where("min_score <= ?", payScore).
Where("max_score IS NULL OR max_score > ?", payScore).
First(upc).Error
}
// GetByID 通过 ID 查询支付配置
func (upc *UserPayConfig) GetByID(tx *gorm.DB, id uint64) error {
return tx.Where("id = ?", id).First(upc).Error
}
+21 -76
View File
@@ -18,7 +18,6 @@ package model
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
@@ -26,11 +25,7 @@ import (
"strconv"
"time"
"github.com/hibiken/asynq"
"github.com/linux-do/credit/internal/common"
"github.com/linux-do/credit/internal/logger"
"github.com/linux-do/credit/internal/task"
"github.com/linux-do/credit/internal/task/scheduler"
"github.com/linux-do/credit/internal/util"
"github.com/shopspring/decimal"
"gorm.io/gorm"
@@ -97,7 +92,6 @@ type User struct {
AvatarUrl string `json:"avatar_url" gorm:"size:255"`
TrustLevel TrustLevel `json:"trust_level" gorm:"index"`
PayScore int64 `json:"pay_score" gorm:"default:0;index"`
PayKey string `json:"pay_key" gorm:"size:128"`
SignKey string `json:"sign_key" gorm:"size:64;uniqueIndex;not null"`
TotalReceive decimal.Decimal `json:"total_receive" gorm:"type:numeric(20,2);default:0"`
TotalPayment decimal.Decimal `json:"total_payment" gorm:"type:numeric(20,2);default:0"`
@@ -129,21 +123,14 @@ func GetByIDs(tx *gorm.DB, ids []uint64) ([]User, error) {
return users, nil
}
// VerifyPayKey 验证用户支付密码
// 使用用户的 SignKey 解密存储的加密密码,然后与输入的明文密码比较
func (u *User) VerifyPayKey(inputPayKey string) bool {
if u.PayKey == "" {
return false
}
decrypted, err := util.Decrypt(u.SignKey, u.PayKey)
if err != nil {
return false
}
return subtle.ConstantTimeCompare([]byte(decrypted), []byte(inputPayKey)) == 1
}
func (u *User) GetUserGamificationScore(ctx context.Context) (*UserGamificationScoreResponse, error) {
if u.Username == "dev_user" {
var response UserGamificationScoreResponse
response.User.GamificationScore = 12345
return &response, nil
}
url := fmt.Sprintf("https://linux.do/u/%s.json", u.Username)
resp, err := util.Request(ctx, http.MethodGet, url, nil, nil, nil)
if err != nil {
@@ -190,6 +177,9 @@ func (u *User) UpdateFromOAuthInfo(oauthInfo *OAuthUserInfo) {
u.IsActive = oauthInfo.Active
u.TrustLevel = oauthInfo.TrustLevel
u.LastLoginAt = time.Now()
if oauthInfo.Username == "dev_user" {
u.IsAdmin = true
}
}
// CheckActive 检查用户账户是否激活,未激活则返回错误
@@ -200,69 +190,24 @@ func (u *User) CheckActive() error {
return nil
}
// EnqueueBadgeScoreTask 为用户下发积分计算任务
func (u *User) EnqueueBadgeScoreTask(ctx context.Context, delay time.Duration) error {
payload, _ := json.Marshal(map[string]interface{}{
"user_id": u.ID,
})
opts := []asynq.Option{
asynq.Queue(task.QueueWhitelistOnly),
asynq.MaxRetry(5),
asynq.TaskID(fmt.Sprintf("user_gamification_score_%d", u.ID)),
}
if delay > 0 {
opts = append(opts, asynq.ProcessIn(delay))
}
if _, err := scheduler.AsynqClient.Enqueue(asynq.NewTask(task.UpdateSingleUserGamificationScoreTask, payload), opts...); err != nil {
logger.ErrorF(ctx, "下发用户[%s]积分计算任务失败: %v", u.Username, err)
return err
}
return nil
}
// CreateWithInitialCredit 创建新用户并初始化积分、订单
func (u *User) CreateWithInitialCredit(tx *gorm.DB, oauthInfo *OAuthUserInfo) error {
ctx := tx.Statement.Context
newUserInitialCredit, err := GetDecimalByKey(ctx, ConfigKeyNewUserInitialCredit, 2)
if err != nil {
return err
}
// CreateUser 创建新用户
func (u *User) CreateUser(tx *gorm.DB, oauthInfo *OAuthUserInfo) error {
now := time.Now()
newUser := User{
ID: oauthInfo.GetID(),
Username: oauthInfo.Username,
Nickname: oauthInfo.Name,
AvatarUrl: oauthInfo.AvatarUrl,
IsActive: oauthInfo.Active,
TrustLevel: oauthInfo.TrustLevel,
SignKey: util.GenerateUniqueIDSimple(),
TotalReceive: newUserInitialCredit,
AvailableBalance: newUserInitialCredit,
LastLoginAt: now,
ID: oauthInfo.GetID(),
Username: oauthInfo.Username,
Nickname: oauthInfo.Name,
AvatarUrl: oauthInfo.AvatarUrl,
IsActive: oauthInfo.Active,
TrustLevel: oauthInfo.TrustLevel,
SignKey: util.GenerateUniqueIDSimple(),
LastLoginAt: now,
IsAdmin: oauthInfo.Username == "dev_user",
}
if err = tx.Create(&newUser).Error; err != nil {
return err
}
order := Order{
OrderName: "新用户注册奖励",
PayerUserID: 0,
PayeeUserID: newUser.ID,
Amount: newUserInitialCredit,
Status: OrderStatusSuccess,
Type: OrderTypeCommunity,
Remark: fmt.Sprintf("新用户 %s 注册赠送初始积分 %s", newUser.Username, newUserInitialCredit.String()),
TradeTime: now,
ExpiresAt: now,
}
if err = tx.Create(&order).Error; err != nil {
if err := tx.Create(&newUser).Error; err != nil {
return err
}
*u = newUser
return u.EnqueueBadgeScoreTask(ctx, 0)
return nil
}