* fix:修复响应污染问题 * fix:修改存储相关文件为中文注释 * fix:修复二次退款问题 * fix:修复多个考虑不全面问题 1.误删用户数据 2.R2存储默认区域 3.删除失败导致孤儿记录 4.网络抖动事变时重试 5.前端无限次请求获取图片 6.文件管理权限 * fix:修复字符串截断异常问题和文件下载超时问题 * fix:修复多图问题 * fix:异步生成图片时未配置oss提示错误信息 * fix:修复关闭异步后同步生成出错问题 * feat:新增文件存储测试功能,防止设置后不生效 * fix:修复同步图片转异步后计费错误 * fix:修复无下限扣款问题 * fix:修复结算重复扣款问题 * fix:修复计费出错导致的0费用结算 * fix:修复回调双重履约 * fix:修复预扣事务 * fix:修复费用字段类型问题 * fix:修复缓存问题 * fix:修复安全问题 * fix:修复数据库操作事务问题 * refactor:重构事务使用方式,改为官方推荐的dao形式 * fix:修复余额更新隐患 * fix:修复N+1查询问题 * fix:修复密钥安全问题 * fix:结算失败时打印错误日志 * improve:优化扣款 * fix:修复用户接口越权 --------- Co-authored-by: chengqian <wuchengqian@kkidc.com>
284 lines
8.4 KiB
Go
284 lines
8.4 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/gogf/gf/v2/database/gdb"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
v1 "github.com/qianfree/team-api/api/admin/v1"
|
|
"github.com/qianfree/team-api/internal/dao"
|
|
do "github.com/qianfree/team-api/internal/model/do"
|
|
|
|
"github.com/qianfree/team-api/internal/logic/common"
|
|
"github.com/qianfree/team-api/internal/logic/payment"
|
|
"github.com/qianfree/team-api/internal/utility/export"
|
|
)
|
|
|
|
// ListOrders 获取全部订单列表
|
|
func (s *sAdmin) ListOrders(ctx context.Context, req *v1.OrderListReq) (*v1.OrderListRes, error) {
|
|
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
|
|
|
query := dao.OrdOrders.Ctx(ctx)
|
|
if req.Status != "" {
|
|
query = query.Where("status", req.Status)
|
|
}
|
|
if req.TenantID != "" {
|
|
query = query.Where("tenant_id", req.TenantID)
|
|
}
|
|
|
|
var total int
|
|
orders := make([]*v1.OrderItem, 0)
|
|
err := query.OrderDesc("created_at").
|
|
Page(page, pageSize).
|
|
ScanAndCount(&orders, &total, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &v1.OrderListRes{
|
|
List: orders,
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}, nil
|
|
}
|
|
|
|
// GetOrder 获取订单详情
|
|
func (s *sAdmin) GetOrder(ctx context.Context, req *v1.OrderDetailReq) (*v1.OrderDetailRes, error) {
|
|
var order *v1.OrderItem
|
|
err := dao.OrdOrders.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Scan(&order)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if order == nil {
|
|
return nil, common.NewNotFoundError("订单")
|
|
}
|
|
return &v1.OrderDetailRes{OrderItem: order}, nil
|
|
}
|
|
|
|
// RefundOrder 发起退款
|
|
func (s *sAdmin) RefundOrder(ctx context.Context, req *v1.OrderRefundReq) (*v1.OrderRefundRes, error) {
|
|
adminUserID := common.GetCtxUserID(ctx)
|
|
|
|
var order *struct {
|
|
TenantID int64 `json:"tenant_id"`
|
|
FinalAmount float64 `json:"final_amount"`
|
|
Status string `json:"status"`
|
|
PaymentChannel string `json:"payment_channel"`
|
|
}
|
|
err := dao.OrdOrders.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Scan(&order)
|
|
if err = common.IgnoreScanNoRows(err); err != nil {
|
|
return nil, err
|
|
}
|
|
if order == nil {
|
|
return nil, common.NewNotFoundError("订单")
|
|
}
|
|
if order.Status != "paid" && order.Status != "fulfilled" {
|
|
return nil, common.NewBadRequestError("订单状态不支持退款")
|
|
}
|
|
|
|
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
_, err := dao.OrdOrders.Ctx(ctx).
|
|
Where("id", req.Id).
|
|
Data(do.OrdOrders{
|
|
Status: "refunding",
|
|
}).Update()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = dao.OrdRefunds.Ctx(ctx).Insert(do.OrdRefunds{
|
|
OrderId: req.Id,
|
|
TenantId: order.TenantID,
|
|
Amount: order.FinalAmount,
|
|
Reason: req.Reason,
|
|
Status: "approved",
|
|
PaymentChannel: order.PaymentChannel,
|
|
ApprovedBy: adminUserID,
|
|
ApprovedAt: gtime.Now(),
|
|
})
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &v1.OrderRefundRes{}, nil
|
|
}
|
|
|
|
// OrderComplete 手动完成订单
|
|
func (s *sAdmin) OrderComplete(ctx context.Context, req *v1.OrderCompleteReq) (*v1.OrderCompleteRes, error) {
|
|
adminUserID := common.GetCtxUserID(ctx)
|
|
|
|
orderNo, err := getOrderForComplete(ctx, req.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payment.LockOrder(orderNo)
|
|
defer payment.UnlockOrder(orderNo)
|
|
|
|
if err := markOrderPaidByAdmin(ctx, req.Id, adminUserID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := payment.FulfillOrder(ctx, req.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &v1.OrderCompleteRes{}, nil
|
|
}
|
|
|
|
// GetPaymentChannels 获取所有渠道配置(单例模式,从 sys_options 读取)
|
|
func (s *sAdmin) GetPaymentChannels(ctx context.Context, _ *v1.PaymentChannelListReq) (*v1.PaymentChannelListRes, error) {
|
|
return &v1.PaymentChannelListRes{List: payment.ListAllChannels(ctx)}, nil
|
|
}
|
|
|
|
// SavePaymentChannel 保存指定渠道的配置(整体覆盖)
|
|
func (s *sAdmin) SavePaymentChannel(ctx context.Context, req *v1.PaymentChannelSaveReq) (*v1.PaymentChannelSaveRes, error) {
|
|
// 校验 config JSON 是否合法
|
|
_, err := payment.ParseChannelConfig(req.Channel, req.Config)
|
|
if err != nil {
|
|
return nil, common.NewBadRequestError("配置 JSON 格式无效: " + err.Error())
|
|
}
|
|
if err := payment.SaveChannelConfig(ctx, req.Channel, req.Config); err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1.PaymentChannelSaveRes{}, nil
|
|
}
|
|
|
|
// GetPaymentSettings 获取全局支付设置。
|
|
func (s *sAdmin) GetPaymentSettings(ctx context.Context, _ *v1.PaymentSettingsGetReq) (*v1.PaymentSettingsGetRes, error) {
|
|
settings, err := payment.GetGlobalPaymentSettings(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1.PaymentSettingsGetRes{
|
|
AmountOptions: settings.AmountOptions,
|
|
AmountDiscount: settings.AmountDiscount,
|
|
MinTopUp: settings.MinTopUp,
|
|
Currency: settings.Currency,
|
|
CallbackBaseURL: settings.CallbackBaseURL,
|
|
}, nil
|
|
}
|
|
|
|
// UpdatePaymentSettings 更新全局支付设置。
|
|
func (s *sAdmin) UpdatePaymentSettings(ctx context.Context, req *v1.PaymentSettingsUpdateReq) (*v1.PaymentSettingsUpdateRes, error) {
|
|
settings := &payment.GlobalPaymentSettings{
|
|
AmountOptions: req.AmountOptions,
|
|
AmountDiscount: req.AmountDiscount,
|
|
MinTopUp: req.MinTopUp,
|
|
Currency: req.Currency,
|
|
CallbackBaseURL: req.CallbackBaseURL,
|
|
}
|
|
if err := payment.SaveGlobalPaymentSettings(ctx, settings); err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1.PaymentSettingsUpdateRes{}, nil
|
|
}
|
|
|
|
// getOrderForComplete 获取待完成的订单信息(供 OrderComplete 方法内部调用)。
|
|
func getOrderForComplete(ctx context.Context, orderID int64) (orderNo string, err error) {
|
|
var order *struct {
|
|
OrderNo string `json:"order_no"`
|
|
Status string `json:"status"`
|
|
}
|
|
err = dao.OrdOrders.Ctx(ctx).
|
|
Where("id", orderID).Scan(&order)
|
|
if err = common.IgnoreScanNoRows(err); err != nil {
|
|
return "", err
|
|
}
|
|
if order == nil {
|
|
return "", common.NewNotFoundError("订单")
|
|
}
|
|
if order.Status != "pending" {
|
|
return "", common.NewBadRequestError("订单状态不是待支付,无法完成")
|
|
}
|
|
return order.OrderNo, nil
|
|
}
|
|
|
|
// markOrderPaidByAdmin 将订单标记为已支付(管理员手动完成)。
|
|
func markOrderPaidByAdmin(ctx context.Context, orderID int64, adminUserID int64) error {
|
|
_, err := dao.OrdOrders.Ctx(ctx).
|
|
Where("id", orderID).
|
|
Where("status", "pending").
|
|
Data(do.OrdOrders{
|
|
Status: "paid",
|
|
PaidAt: gtime.Now(),
|
|
PaymentNo: "ADMIN_" + fmt.Sprint(adminUserID),
|
|
}).Update()
|
|
return err
|
|
}
|
|
|
|
// ExportOrders exports order list to CSV or Excel.
|
|
func (s *sAdmin) ExportOrders(ctx context.Context, req *v1.OrderExportReq) (*v1.OrderExportRes, error) {
|
|
columns := []export.Column{
|
|
{Field: "id", Header: "ID"},
|
|
{Field: "order_no", Header: "订单号"},
|
|
{Field: "tenant_id", Header: "租户ID"},
|
|
{Field: "order_type", Header: "订单类型"},
|
|
{Field: "final_amount", Header: "最终金额"},
|
|
{Field: "payment_channel", Header: "支付渠道"},
|
|
{Field: "status", Header: "状态"},
|
|
{Field: "created_at", Header: "创建时间"},
|
|
}
|
|
|
|
config := export.Config{
|
|
Format: req.Format,
|
|
Filename: "订单_" + gtime.Now().Format("Ymd_His"),
|
|
Columns: columns,
|
|
}
|
|
|
|
orderFields := "id, order_no, tenant_id, order_type, final_amount, payment_channel, status, created_at"
|
|
|
|
return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) {
|
|
offset := 0
|
|
for {
|
|
query := dao.OrdOrders.Ctx(ctx)
|
|
if req.Status != "" {
|
|
query = query.Where("status", req.Status)
|
|
}
|
|
if req.TenantID != "" {
|
|
query = query.Where("tenant_id", req.TenantID)
|
|
}
|
|
var batch []struct {
|
|
Id int64 `json:"id"`
|
|
OrderNo string `json:"order_no"`
|
|
TenantId int64 `json:"tenant_id"`
|
|
OrderType string `json:"order_type"`
|
|
FinalAmount float64 `json:"final_amount"`
|
|
PaymentChannel string `json:"payment_channel"`
|
|
Status string `json:"status"`
|
|
CreatedAt *gtime.Time `json:"created_at"`
|
|
}
|
|
if err := query.Fields(orderFields).OrderDesc("created_at").Limit(1000).Offset(offset).Scan(&batch); err != nil {
|
|
return
|
|
}
|
|
for _, o := range batch {
|
|
if !yield(map[string]any{
|
|
"id": o.Id,
|
|
"order_no": o.OrderNo,
|
|
"tenant_id": o.TenantId,
|
|
"order_type": o.OrderType,
|
|
"final_amount": o.FinalAmount,
|
|
"payment_channel": o.PaymentChannel,
|
|
"status": o.Status,
|
|
"created_at": o.CreatedAt.String(),
|
|
}) {
|
|
return
|
|
}
|
|
}
|
|
if len(batch) < 1000 {
|
|
break
|
|
}
|
|
offset += 1000
|
|
}
|
|
})
|
|
}
|