功能:完善登录安全与客服手动下单

This commit is contained in:
yml2213
2026-08-13 12:40:04 +08:00
parent aea65a2fd3
commit 8c47b961e1
10 changed files with 161 additions and 43 deletions
+18
View File
@@ -43,3 +43,21 @@ func (h *AuthHandler) Profile(c *gin.Context) {
}
response.OK(c, user)
}
type changePasswordReq struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
func (h *AuthHandler) ChangePassword(c *gin.Context) {
var req changePasswordReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请填写当前密码和至少 8 位的新密码")
return
}
if err := h.svc.ChangePassword(middleware.GetUserID(c), req.CurrentPassword, req.NewPassword); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, nil)
}
+10
View File
@@ -101,6 +101,16 @@ func (h *MerchantHandler) ListOrders(c *gin.Context) {
response.Page(c, list, total, page, size)
}
// ListManualOrderProducts returns active products for order creation without granting product-management access.
func (h *MerchantHandler) ListManualOrderProducts(c *gin.Context) {
list, _, err := h.merchantSvc.ListMerchantProducts(middleware.GetMerchantID(c), 1, 100, true)
if err != nil {
response.ServerError(c, err.Error())
return
}
response.OK(c, list)
}
type merchantManualOrderReq struct {
ClientOrderNo string `json:"client_order_no" binding:"required,max=96"`
SKU string `json:"sku" binding:"required,max=96"`
+2
View File
@@ -146,6 +146,7 @@ func Setup(h *Handlers) *gin.Engine {
auth.Use(middleware.Tenant(h.Tenant))
{
auth.GET("/auth/profile", h.Auth.Profile)
auth.PUT("/auth/password", h.Auth.ChangePassword)
auth.GET("/dashboard", h.Dashboard.Dashboard)
// 商户后台:商户就是平台下游客户,成员只代表该商户内部员工。
@@ -155,6 +156,7 @@ func Setup(h *Handlers) *gin.Engine {
merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.ListProducts)
merchant.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProductStatus)
merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListOrders)
merchant.GET("/orders/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListManualOrderProducts)
merchant.POST("/orders/manual", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateManualOrder)
merchant.POST("/orders/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateTestOrder)
merchant.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.GetDeliveryLink)
+32
View File
@@ -2,6 +2,7 @@ package service
import (
"errors"
"strings"
"affiliate_dash/internal/model"
"affiliate_dash/internal/pkg/jwt"
@@ -54,6 +55,37 @@ func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
return &user, nil
}
// ChangePassword only changes the current account after its existing password is verified.
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {
if userID == 0 {
return errors.New("无效的用户身份")
}
if len(newPassword) < 8 {
return errors.New("新密码至少 8 位")
}
if strings.TrimSpace(newPassword) == "" {
return errors.New("新密码不能为空")
}
var user model.User
if err := s.db.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("用户不存在")
}
return err
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)); err != nil {
return errors.New("当前密码错误")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(newPassword)); err == nil {
return errors.New("新密码不能与当前密码相同")
}
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
return s.db.Model(&user).Update("password_hash", string(hash)).Error
}
func (s *AuthService) EnsureAdmin() error {
var count int64
s.db.Model(&model.User{}).Where("role = ?", model.RoleAdmin).Count(&count)
+28
View File
@@ -6,6 +6,8 @@ import (
"testing"
"affiliate_dash/internal/model"
"golang.org/x/crypto/bcrypt"
)
func TestCreateAPIClientEnforcesPerMerchantLimit(t *testing.T) {
@@ -174,3 +176,29 @@ func TestMemberCanBeUpdatedOrRemovedWithoutDeletingAccount(t *testing.T) {
t.Fatalf("member relationship should be removed, count=%d err=%v", count, err)
}
}
func TestAuthServiceChangePasswordVerifiesCurrentPassword(t *testing.T) {
db := newServiceTestDB(t)
tenant := NewTenantService(db)
auth := NewAuthService(db, nil, tenant)
user, err := NewUserService(db, tenant).Create("password-owner", "old-password", "密码管理员", model.RoleAdmin, 0)
if err != nil {
t.Fatalf("create user: %v", err)
}
if err := auth.ChangePassword(user.ID, "wrong-password", "new-password"); err == nil {
t.Fatal("expected current password validation error")
}
if err := auth.ChangePassword(user.ID, "old-password", "new-password"); err != nil {
t.Fatalf("change password: %v", err)
}
var saved model.User
if err := db.First(&saved, user.ID).Error; err != nil {
t.Fatalf("load updated user: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("new-password")); err != nil {
t.Fatalf("new password should match: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("old-password")); err == nil {
t.Fatal("old password should no longer match")
}
}