mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
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 order
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type TransactionListRequest struct {
|
||||
Page int `json:"page" form:"page" binding:"min=1"`
|
||||
PageSize int `json:"page_size" form:"page_size" binding:"min=1,max=100"`
|
||||
Types []string `json:"types" form:"types" binding:"omitempty,dive,oneof=receive payment transfer community online test distribute red_envelope_send red_envelope_receive red_envelope_refund"`
|
||||
Statuses []string `json:"statuses" form:"statuses" binding:"omitempty,dive,oneof=success pending failed expired disputing refund refused"`
|
||||
ClientID string `json:"client_id" form:"client_id" binding:"omitempty"`
|
||||
StartTime *time.Time `json:"startTime" form:"startTime" binding:"omitempty"`
|
||||
EndTime *time.Time `json:"endTime" form:"endTime" binding:"omitempty,gtfield=StartTime"`
|
||||
ID *uint64 `json:"id,string" form:"id" binding:"omitempty"`
|
||||
OrderName string `json:"order_name" form:"order_name" binding:"omitempty"`
|
||||
PayerUsername string `json:"payer_username" form:"payer_username" binding:"omitempty"`
|
||||
PayeeUsername string `json:"payee_username" form:"payee_username" binding:"omitempty"`
|
||||
PayeeTransferStatus model.OrderTransferStatus `json:"payee_transfer_status" form:"payee_transfer_status" binding:"omitempty,oneof=pending completed"`
|
||||
}
|
||||
|
||||
type TransactionListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Orders []struct {
|
||||
model.Order
|
||||
AppName string `json:"app_name"`
|
||||
AppHomepageURL string `json:"app_homepage_url"`
|
||||
AppDescription string `json:"app_description"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
DisputeID *uint64 `json:"dispute_id,string"`
|
||||
PayerUsername string `json:"payer_username"`
|
||||
PayeeUsername string `json:"payee_username"`
|
||||
PayerAvatarURL string `json:"payer_avatar_url"`
|
||||
PayeeAvatarURL string `json:"payee_avatar_url"`
|
||||
PayeeTransferStatus string `json:"payee_transfer_status"`
|
||||
PayeeTransferAt string `json:"payee_transfer_at"`
|
||||
} `json:"orders"`
|
||||
}
|
||||
|
||||
// ListTransactions 获取交易列表
|
||||
// @Tags order
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body TransactionListRequest false "request body"
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Router /api/v1/order/transactions [post]
|
||||
func ListTransactions(c *gin.Context) {
|
||||
var req TransactionListRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
user, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
|
||||
baseQuery := db.DB(c.Request.Context()).Model(&model.Order{}).
|
||||
Select("orders.*, merchant_api_keys.app_name, merchant_api_keys.app_homepage_url, merchant_api_keys.app_description, merchant_api_keys.redirect_uri, disputes.id as dispute_id, payer_user.username as payer_username, payee_user.username as payee_username, payer_user.avatar_url as payer_avatar_url, payee_user.avatar_url as payee_avatar_url, order_transfers.status as payee_transfer_status, order_transfers.transfer_at as payee_transfer_at").
|
||||
Joins("LEFT JOIN merchant_api_keys ON orders.client_id = merchant_api_keys.client_id").
|
||||
Joins("LEFT JOIN disputes ON orders.id = disputes.order_id").
|
||||
Joins("LEFT JOIN users as payer_user ON orders.payer_user_id = payer_user.id").
|
||||
Joins("LEFT JOIN users as payee_user ON orders.payee_user_id = payee_user.id").
|
||||
Joins("LEFT JOIN order_transfers ON orders.id = order_transfers.order_id")
|
||||
|
||||
clientIDHandled := false
|
||||
if len(req.Types) > 0 {
|
||||
// 使用 IN 查询支持多个类型
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
for _, t := range req.Types {
|
||||
orderType := model.OrderType(t)
|
||||
|
||||
switch orderType {
|
||||
case model.OrderTypeReceive:
|
||||
// receive 类型:查询当前用户作为收款方的 payment 订单
|
||||
conditions = append(conditions, "(orders.type = ? AND orders.payee_user_id = ?)")
|
||||
args = append(args, model.OrderTypePayment, user.ID)
|
||||
case model.OrderTypeCommunity, model.OrderTypeRedEnvelopeRefund, model.OrderTypeRedEnvelopeReceive:
|
||||
// community、red_envelope_refund、red_envelope_receive 类型:查询当前用户作为收款方的订单
|
||||
conditions = append(conditions, "(orders.type = ? AND orders.payee_user_id = ?)")
|
||||
args = append(args, orderType, user.ID)
|
||||
case model.OrderTypeOnline:
|
||||
// online 类型:商家可查看自己 client_id 的所有订单,普通用户只能查看与自己相关的订单
|
||||
if req.ClientID != "" {
|
||||
clientIDHandled = true
|
||||
var count int64
|
||||
if err := db.DB(c.Request.Context()).Model(&model.MerchantAPIKey{}).
|
||||
Where("client_id = ? AND user_id = ?", req.ClientID, user.ID).
|
||||
Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
conditions = append(conditions, "(orders.type = ? AND orders.client_id = ?)")
|
||||
args = append(args, orderType, req.ClientID)
|
||||
} else {
|
||||
conditions = append(conditions, "(orders.type = ? AND orders.client_id = ? AND (orders.payer_user_id = ? OR orders.payee_user_id = ?))")
|
||||
args = append(args, orderType, req.ClientID, user.ID, user.ID)
|
||||
}
|
||||
} else {
|
||||
conditions = append(conditions, "(orders.type = ? AND (orders.payer_user_id = ? OR orders.payee_user_id = ?))")
|
||||
args = append(args, orderType, user.ID, user.ID)
|
||||
}
|
||||
case model.OrderTypePayment, model.OrderTypeTransfer, model.OrderTypeTest, model.OrderTypeRedEnvelopeSend:
|
||||
// payment、transfer、test、red_envelope_send 类型:查询当前用户作为付款方的订单
|
||||
conditions = append(conditions, "(orders.type = ? AND orders.payer_user_id = ?)")
|
||||
args = append(args, orderType, user.ID)
|
||||
case model.OrderTypeDistribute:
|
||||
// distribute 类型:查询当前用户作为付款方或者收款方的订单
|
||||
conditions = append(conditions, "((orders.type = ? AND orders.payer_user_id = ?) OR (orders.type = ? AND orders.payee_user_id = ?))")
|
||||
args = append(args, orderType, user.ID, orderType, user.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(conditions) > 0 {
|
||||
combinedCondition := conditions[0]
|
||||
for i := 1; i < len(conditions); i++ {
|
||||
combinedCondition += " OR " + conditions[i]
|
||||
}
|
||||
baseQuery = baseQuery.Where(combinedCondition, args...)
|
||||
}
|
||||
} else {
|
||||
// 查询所有与当前用户相关的订单,但排除用户作为payer的red_envelope_receive订单
|
||||
baseQuery = baseQuery.Where(
|
||||
"orders.payee_user_id = ? OR (orders.payer_user_id = ? AND orders.type != ?)",
|
||||
user.ID, user.ID, model.OrderTypeRedEnvelopeReceive,
|
||||
)
|
||||
}
|
||||
|
||||
if len(req.Statuses) > 0 {
|
||||
baseQuery = baseQuery.Where("orders.status IN ?", req.Statuses)
|
||||
}
|
||||
|
||||
if req.ClientID != "" && !clientIDHandled {
|
||||
baseQuery = baseQuery.Where("orders.client_id = ?", req.ClientID)
|
||||
}
|
||||
if req.ID != nil {
|
||||
baseQuery = baseQuery.Where("orders.id = ?", req.ID)
|
||||
}
|
||||
if req.OrderName != "" {
|
||||
baseQuery = baseQuery.Where("orders.order_name LIKE ?", req.OrderName+"%")
|
||||
}
|
||||
if req.PayerUsername != "" {
|
||||
baseQuery = baseQuery.Where("payer_user.username LIKE ?", req.PayerUsername+"%")
|
||||
}
|
||||
if req.PayeeUsername != "" {
|
||||
baseQuery = baseQuery.Where("payee_user.username LIKE ?", req.PayeeUsername+"%")
|
||||
}
|
||||
if req.PayeeTransferStatus != "" {
|
||||
switch req.PayeeTransferStatus {
|
||||
case model.OrderTransferStatusPending:
|
||||
baseQuery = baseQuery.Where("order_transfers.status = ?", model.OrderTransferStatusPending)
|
||||
case model.OrderTransferStatusCompleted:
|
||||
baseQuery = baseQuery.Where("order_transfers.status = ? OR order_transfers.status IS NULL", model.OrderTransferStatusCompleted)
|
||||
}
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
baseQuery = baseQuery.Where("orders.created_at >= ?", req.StartTime)
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
baseQuery = baseQuery.Where("orders.created_at <= ?", req.EndTime)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := baseQuery.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response := &TransactionListResponse{
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := baseQuery.Order("orders.created_at DESC").Offset(offset).Limit(req.PageSize).Find(&response.Orders).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 转换订单类型:从收款方视角看,payment 订单应该显示为 receive
|
||||
// 并更新 Payee_transfer_status,兼容为空的场景
|
||||
for i := range response.Orders {
|
||||
if response.Orders[i].PayeeTransferStatus == "" {
|
||||
response.Orders[i].PayeeTransferStatus = string(model.OrderTransferStatusCompleted)
|
||||
}
|
||||
if response.Orders[i].Type == model.OrderTypePayment && response.Orders[i].PayeeUserID == user.ID {
|
||||
response.Orders[i].Type = model.OrderTypeReceive
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(response))
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
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 order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"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/service"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// HandleSyncOrdersToClickHouse 同步订单数据
|
||||
func HandleSyncOrdersToClickHouse(ctx context.Context, t *asynq.Task) error {
|
||||
if !config.Config.ClickHouse.Enabled {
|
||||
logger.InfoF(ctx, "ClickHouse 未启用,跳过订单同步")
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
StartTime *time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time"`
|
||||
}
|
||||
_ = json.Unmarshal(t.Payload(), &payload)
|
||||
|
||||
var startOfDay, endOfDay time.Time
|
||||
if payload.StartTime != nil && payload.EndTime != nil {
|
||||
startOfDay = *payload.StartTime
|
||||
endOfDay = *payload.EndTime
|
||||
} else {
|
||||
now := time.Now()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
startOfDay = time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, yesterday.Location())
|
||||
endOfDay = startOfDay.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "开始同步订单到 ClickHouse: %s ~ %s", startOfDay.Format("2006-01-02 15:04:05"), endOfDay.Format("2006-01-02 15:04:05"))
|
||||
|
||||
pageSize := 10000
|
||||
lastID := uint64(0)
|
||||
totalSynced := 0
|
||||
|
||||
for {
|
||||
var orders []model.Order
|
||||
if err := db.DB(ctx).
|
||||
Where("updated_at >= ? AND updated_at < ? AND id > ?", startOfDay, endOfDay, lastID).
|
||||
Order("id ASC").
|
||||
Limit(pageSize).
|
||||
Find(&orders).Error; err != nil {
|
||||
logger.ErrorF(ctx, "查询订单失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(orders) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if err := batchInsertToClickHouse(ctx, orders); err != nil {
|
||||
logger.ErrorF(ctx, "写入 ClickHouse 失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
totalSynced += len(orders)
|
||||
lastID = orders[len(orders)-1].ID
|
||||
|
||||
logger.InfoF(ctx, "已同步 %d 条订单,当前进度 ID: %d", len(orders), lastID)
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "订单同步完成,共同步 %d 条", totalSynced)
|
||||
return nil
|
||||
}
|
||||
|
||||
// batchInsertToClickHouse 批量写入订单
|
||||
func batchInsertToClickHouse(ctx context.Context, orders []model.Order) error {
|
||||
batch, err := db.ChConn.PrepareBatch(ctx, `
|
||||
INSERT INTO orders (
|
||||
id, order_name, merchant_order_no, client_id,
|
||||
payer_user_id, payee_user_id, amount,
|
||||
status, type, remark, payment_type,
|
||||
trade_time, expires_at, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, order := range orders {
|
||||
if err := batch.Append(
|
||||
order.ID,
|
||||
order.OrderName,
|
||||
util.DerefString(order.MerchantOrderNo),
|
||||
order.ClientID,
|
||||
order.PayerUserID,
|
||||
order.PayeeUserID,
|
||||
order.Amount,
|
||||
string(order.Status),
|
||||
string(order.Type),
|
||||
order.Remark,
|
||||
order.PaymentType,
|
||||
order.TradeTime,
|
||||
order.ExpiresAt,
|
||||
order.CreatedAt,
|
||||
order.UpdatedAt,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return batch.Send()
|
||||
}
|
||||
|
||||
// HandleSettlePendingPayments 处理到期的延迟到账订单
|
||||
func HandleSettlePendingPayments(ctx context.Context, _ *asynq.Task) error {
|
||||
batchSize := 500
|
||||
totalSettled := 0
|
||||
|
||||
for {
|
||||
orders, err := model.GetDueTransferOrders(ctx, batchSize)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "查询到期待结算订单失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(orders) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, order := range orders {
|
||||
if err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 锁定行
|
||||
var lockedOrderTransfer model.OrderTransfer
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}).
|
||||
Where("id = ? AND status = ?", order.ID, model.OrderTransferStatusPending).
|
||||
First(&lockedOrderTransfer).Error; err != nil {
|
||||
return fmt.Errorf("lock order failed: %w", err)
|
||||
}
|
||||
|
||||
// 移动到可用余额
|
||||
if err := service.SettlePendingToAvailable(tx, lockedOrderTransfer.PayeeUserID, lockedOrderTransfer.Amount); err != nil {
|
||||
return fmt.Errorf("settle balance failed: %w", err)
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
if err := tx.Model(&lockedOrderTransfer).Updates(
|
||||
map[string]interface{}{
|
||||
"transfer_at": time.Now(),
|
||||
"status": model.OrderTransferStatusCompleted,
|
||||
},
|
||||
).Error; err != nil {
|
||||
return fmt.Errorf("update order status failed: %w", err)
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "订单[ID:%d]延迟到账结算成功: 商户[ID:%d] 金额[%s]", lockedOrderTransfer.ID, lockedOrderTransfer.PayeeUserID, lockedOrderTransfer.Amount.String())
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.ErrorF(ctx, "结算订单[ID:%d]失败: %v", order.ID, err)
|
||||
continue
|
||||
}
|
||||
totalSettled++
|
||||
}
|
||||
}
|
||||
|
||||
if totalSettled > 0 {
|
||||
logger.InfoF(ctx, "延迟到账结算完成,共结算 %d 笔", totalSettled)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user