From cfbe751af792a05a4bed0e4eefaa08ab94eb7ec9 Mon Sep 17 00:00:00 2001 From: luorijun Date: Sat, 11 Apr 2026 14:10:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B4=A6=E5=8D=95=E4=B8=8E?= =?UTF-8?q?=E4=BA=A4=E6=98=93=E6=8E=A5=E5=8F=A3=E9=97=AE=E9=A2=98=20&=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E6=9D=83=E9=99=90=E7=9A=84=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ web/auth/account.go | 3 +++ web/auth/endpoints.go | 5 +++++ web/core/scopes.go | 9 ++++---- web/handlers/trade.go | 46 ++++++++++++++++++++++++++++++++++----- web/handlers/user.go | 10 +++++++++ web/handlers/whitelist.go | 7 +++--- web/routes.go | 1 + web/services/bill.go | 26 +++++++++++++++++----- web/services/resource.go | 12 +++++----- web/services/trade.go | 13 ++++++----- web/services/user.go | 5 +++-- 12 files changed, 107 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 639ac1d..6e45c22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## TODO +交易信息持久化 + 用户请求需要检查数据权限 用反射实现环境变量解析,以简化函数签名 diff --git a/web/auth/account.go b/web/auth/account.go index cdf84f7..a79707e 100644 --- a/web/auth/account.go +++ b/web/auth/account.go @@ -169,6 +169,9 @@ func adminScopes(admin *m.Admin) ([]string, error) { scopeNames := make([]string, 0, len(scopes)) for _, scope := range scopes { + if scope.Name == "" { + continue + } scopeNames = append(scopeNames, scope.Name) } return scopeNames, nil diff --git a/web/auth/endpoints.go b/web/auth/endpoints.go index 0501951..99c1656 100644 --- a/web/auth/endpoints.go +++ b/web/auth/endpoints.go @@ -356,6 +356,11 @@ func authPassword(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m return nil, err } + // 非锁定管理员,不允许为空权限 + if !admin.Lock && (len(scopes) == 0) { + return nil, ErrAuthorizeInvalidScope // 没有配置权限 + } + // 更新管理员登录时间 admin.LastLogin = u.P(time.Now()) admin.LastLoginIP = ip diff --git a/web/core/scopes.go b/web/core/scopes.go index 6fe6d44..5d638d9 100644 --- a/web/core/scopes.go +++ b/web/core/scopes.go @@ -62,10 +62,11 @@ const ( ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表 ScopeChannelWrite = string("channel:write") // 写入 IP - ScopeTrade = string("trade") // 交易 - ScopeTradeRead = string("trade:read") // 读取交易列表 - ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表 - ScopeTradeWrite = string("trade:write") // 写入交易 + ScopeTrade = string("trade") // 交易 + ScopeTradeRead = string("trade:read") // 读取交易列表 + ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表 + ScopeTradeWrite = string("trade:write") // 写入交易 + ScopeTradeWriteComplete = string("trade:write:complete") // 完成交易 ScopeBill = string("bill") // 账单 ScopeBillRead = string("bill:read") // 读取账单列表 diff --git a/web/handlers/trade.go b/web/handlers/trade.go index d4a87e0..6a86f3e 100644 --- a/web/handlers/trade.go +++ b/web/handlers/trade.go @@ -173,6 +173,8 @@ type PageTradeOfUserByAdminReq struct { CreatedAtEnd *time.Time `json:"created_at_end,omitempty"` } +// ============================================================ + // 创建订单 func TradeCreate(c *fiber.Ctx) error { // 检查权限 @@ -219,6 +221,8 @@ type TradeCreateReq struct { Recharge *s.UpdateBalanceData `json:"recharge,omitempty"` } +// ============================================================ + // 完成订单 func TradeComplete(c *fiber.Ctx) error { // 检查权限 @@ -228,13 +232,13 @@ func TradeComplete(c *fiber.Ctx) error { } // 解析请求参数 - req := new(TradeCompleteReq) - if err := g.Validator.ParseBody(c, req); err != nil { + var req s.TradeRef + if err := g.Validator.ParseBody(c, &req); err != nil { return err } // 检查订单状态 - err = s.Trade.CompleteTrade(authCtx.User, &req.TradeRef) + err = s.Trade.CompleteTrade(authCtx.User, &req) if err != nil { return err } @@ -242,10 +246,40 @@ func TradeComplete(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } -type TradeCompleteReq struct { - s.TradeRef +// 管理员完成订单 +func TradeCompleteByAdmin(c *fiber.Ctx) error { + // 检查权限 + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeTradeWriteComplete) + if err != nil { + return err + } + + // 解析请求参数 + var req struct { + s.TradeRef + UserID int32 `json:"user_id" validate:"required"` + } + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + // 获取用户信息 + user, err := s.User.Get(q.Q, req.UserID) + if err != nil { + return err + } + + // 完成订单 + err = s.Trade.CompleteTrade(user, &req.TradeRef) + if err != nil { + return err + } + + return c.SendStatus(fiber.StatusNoContent) } +// ============================================================ + // 取消订单 func TradeCancel(c *fiber.Ctx) error { // 检查权限 @@ -274,6 +308,8 @@ type TradeCancelReq struct { s.TradeRef } +// ============================================================ + // 检查订单 func TradeCheck(c *fiber.Ctx) error { // 检查权限:sse 接口暂时不检查权限 diff --git a/web/handlers/user.go b/web/handlers/user.go index 3da0272..e96cb9c 100644 --- a/web/handlers/user.go +++ b/web/handlers/user.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "platform/web/auth" "platform/web/core" g "platform/web/globals" @@ -76,6 +77,12 @@ func PageUserByAdmin(c *fiber.Ctx) error { } for _, user := range users { + + if user.IDNo != nil && len(*user.IDNo) == 18 { + var str = *user.IDNo + *user.IDNo = str[:6] + "****" + str[len(str)-2:] + } + if user.Admin != nil { user.Admin = &m.Admin{ Name: user.Admin.Name, @@ -306,6 +313,9 @@ func UpdateUser(c *fiber.Ctx) error { ContactQQ: &req.ContactQQ, ContactWechat: &req.ContactWechat, }) + if errors.Is(err, gorm.ErrDuplicatedKey) { + return core.NewBizErr("用户名或邮箱已被占用") + } if err != nil { return err } diff --git a/web/handlers/whitelist.go b/web/handlers/whitelist.go index 56f73cd..63c391a 100644 --- a/web/handlers/whitelist.go +++ b/web/handlers/whitelist.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "platform/pkg/env" "platform/pkg/u" "platform/web/auth" @@ -92,7 +93,7 @@ func CreateWhitelist(c *fiber.Ctx) error { ip, err := secureAddr(req.Host) if err != nil { - return err + return core.NewBizErr("IP 地址无效", err) } // 创建白名单 @@ -132,7 +133,7 @@ func UpdateWhitelist(c *fiber.Ctx) error { ip, err := secureAddr(req.Host) if err != nil { - return err + return core.NewBizErr("IP 地址无效", err) } // 更新白名单 @@ -201,7 +202,7 @@ func secureAddr(str string) (*orm.Inet, error) { return nil, err } if !ip.IsGlobalUnicast() && env.RunMode != env.RunModeDev { - return nil, fiber.NewError(fiber.StatusBadRequest, "IP 地址不可用") + return nil, errors.New("IP 地址不可用") } return ip, nil } diff --git a/web/routes.go b/web/routes.go index 9bfba7e..159bd13 100644 --- a/web/routes.go +++ b/web/routes.go @@ -192,6 +192,7 @@ func adminRouter(api fiber.Router) { var trade = api.Group("/trade") trade.Post("/page", handlers.PageTradeByAdmin) trade.Post("/page/of-user", handlers.PageTradeOfUserByAdmin) + trade.Post("/complete", handlers.TradeCompleteByAdmin) // bill 账单 var bill = api.Group("/bill") diff --git a/web/services/bill.go b/web/services/bill.go index ec12517..61882bc 100644 --- a/web/services/bill.go +++ b/web/services/bill.go @@ -9,8 +9,8 @@ var Bill = &billService{} type billService struct{} -func (s *billService) CreateForBalance(q *q.Query, uid, tradeId int32, detail *TradeDetail) error { - return q.Bill.Create(&m.Bill{ +func (s *billService) CreateForBalance(q *q.Query, uid, tradeId int32, detail *TradeDetail) (*m.Bill, error) { + bill := &m.Bill{ UserID: uid, BillNo: ID.GenReadable("bil"), TradeID: &tradeId, @@ -18,11 +18,18 @@ func (s *billService) CreateForBalance(q *q.Query, uid, tradeId int32, detail *T Info: &detail.Subject, Amount: detail.Amount, Actual: detail.Actual, - }) + } + + err := q.Bill.Create(bill) + if err != nil { + return nil, err + } + + return bill, nil } -func (s *billService) CreateForResource(q *q.Query, uid, resourceId int32, tradeId *int32, detail *TradeDetail) error { - return q.Bill.Create(&m.Bill{ +func (s *billService) CreateForResource(q *q.Query, uid, resourceId int32, tradeId *int32, detail *TradeDetail) (*m.Bill, error) { + bill := &m.Bill{ UserID: uid, BillNo: ID.GenReadable("bil"), ResourceID: &resourceId, @@ -32,5 +39,12 @@ func (s *billService) CreateForResource(q *q.Query, uid, resourceId int32, trade Info: &detail.Subject, Amount: detail.Amount, Actual: detail.Actual, - }) + } + + err := q.Bill.Create(bill) + if err != nil { + return nil, err + } + + return bill, nil } diff --git a/web/services/resource.go b/web/services/resource.go index 0a29d61..638d208 100644 --- a/web/services/resource.go +++ b/web/services/resource.go @@ -28,11 +28,6 @@ func (s *resourceService) CreateResourceByBalance(user *m.User, data *CreateReso return q.Q.Transaction(func(q *q.Query) error { - // 更新用户余额 - if err := User.UpdateBalance(q, user, detail.Actual.Neg(), "余额购买产品", nil); err != nil { - return core.NewServErr("更新用户余额失败", err) - } - // 保存套餐 resource, err := s.Create(q, user.ID, now, data) if err != nil { @@ -40,11 +35,16 @@ func (s *resourceService) CreateResourceByBalance(user *m.User, data *CreateReso } // 生成账单 - err = Bill.CreateForResource(q, user.ID, resource.ID, nil, detail) + bill, err := Bill.CreateForResource(q, user.ID, resource.ID, nil, detail) if err != nil { return core.NewServErr("生成账单失败", err) } + // 更新用户余额 + if err := User.UpdateBalance(q, user, detail.Actual.Neg(), "余额购买产品", nil, &bill.ID); err != nil { + return core.NewServErr("更新用户余额失败", err) + } + // 核销优惠券 if detail.CouponUserId != nil { err = Coupon.UseCoupon(q, *detail.CouponUserId) diff --git a/web/services/trade.go b/web/services/trade.go index 8a27a26..9885d2d 100644 --- a/web/services/trade.go +++ b/web/services/trade.go @@ -302,17 +302,18 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str switch trade.Type { case m.TradeTypeRecharge: - // 更新用户余额 - if err := User.UpdateBalance(q, user, detail.Actual, "充值余额", nil); err != nil { - return err - } // 生成账单 - err = Bill.CreateForBalance(q, user.ID, trade.ID, &detail) + bill, err := Bill.CreateForBalance(q, user.ID, trade.ID, &detail) if err != nil { return core.NewServErr("生成账单失败", err) } + // 更新用户余额 + if err := User.UpdateBalance(q, user, detail.Actual, "充值余额", nil, &bill.ID); err != nil { + return err + } + case m.TradeTypePurchase: data, ok := detail.Product.(*CreateResourceData) if !ok { @@ -326,7 +327,7 @@ func (s *tradeService) OnCompleteTrade(user *m.User, interNo string, outerNo str } // 生成账单 - err = Bill.CreateForResource(q, user.ID, resource.ID, &trade.ID, &detail) + _, err = Bill.CreateForResource(q, user.ID, resource.ID, &trade.ID, &detail) if err != nil { return core.NewServErr("生成账单失败", err) } diff --git a/web/services/user.go b/web/services/user.go index 30c011c..69532fd 100644 --- a/web/services/user.go +++ b/web/services/user.go @@ -39,11 +39,11 @@ func (s *userService) UpdateBalanceByAdmin(user *m.User, newBalance decimal.Deci } return q.Q.Transaction(func(q *q.Query) error { - return s.UpdateBalance(q, user, amount, "管理员修改余额", adminId) + return s.UpdateBalance(q, user, amount, "管理员修改余额", adminId, nil) }) } -func (s *userService) UpdateBalance(q *q.Query, user *m.User, amount decimal.Decimal, remark string, adminId *int32) error { +func (s *userService) UpdateBalance(q *q.Query, user *m.User, amount decimal.Decimal, remark string, adminId *int32, billId *int32) error { balance := user.Balance.Add(amount) if balance.IsNegative() { return core.NewServErr("用户余额不足") @@ -66,6 +66,7 @@ func (s *userService) UpdateBalance(q *q.Query, user *m.User, amount decimal.Dec err = q.BalanceActivity.Create(&m.BalanceActivity{ UserID: user.ID, AdminID: adminId, + BillID: billId, Amount: amount.StringFixed(2), BalancePrev: user.Balance.StringFixed(2), BalanceCurr: balance.StringFixed(2),