feat: 新增用户自助注册功能
后端新增 POST /api/v1/public/register 接口,支持门店自助注册并自动生成门店编码(S000001 格式);shops 表加 description 字段。营销站新增 /register/ 注册页,含门店信息和管理员账号表单,注册成功后展示门店编码和登录提示。Flutter 登录页底部加「前往官网注册」跳转链接。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,23 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// Register POST /api/v1/public/register
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req service.RegisterInput
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.Register(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
// Refresh POST /api/v1/auth/refresh
|
||||
func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||
var req struct {
|
||||
|
||||
@@ -4,6 +4,7 @@ type Shop struct {
|
||||
Base
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Code string `gorm:"size:50;uniqueIndex" json:"code"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Address string `gorm:"size:255" json:"address"`
|
||||
Phone string `gorm:"size:30" json:"phone"`
|
||||
BusinessHours string `gorm:"size:100" json:"business_hours"`
|
||||
|
||||
@@ -58,6 +58,7 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
public.GET("/products/:public_id", publicH.GetProduct)
|
||||
public.GET("/release", publicH.GetRelease)
|
||||
public.POST("/errors", errorReportH.Submit)
|
||||
public.POST("/register", authH.Register)
|
||||
}
|
||||
|
||||
// 需要 JWT 的路由(ReadOnly 中间件:只读用户不可执行写操作)
|
||||
|
||||
@@ -2,9 +2,11 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -61,6 +63,76 @@ func (s *AuthService) Login(shopCode, username, password string) (*TokenPair, *m
|
||||
return pair, &user, nil
|
||||
}
|
||||
|
||||
// RegisterInput 注册新门店所需参数
|
||||
type RegisterInput struct {
|
||||
ShopName string `json:"shop_name" binding:"required"`
|
||||
Address string `json:"address" binding:"required"`
|
||||
ManagerName string `json:"manager_name" binding:"required"`
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// RegisterResult 注册成功后返回的数据
|
||||
type RegisterResult struct {
|
||||
ShopCode string `json:"shop_code"`
|
||||
ShopName string `json:"shop_name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// Register 自助注册新门店(公开接口,无需认证)
|
||||
func (s *AuthService) Register(in RegisterInput) (*RegisterResult, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result RegisterResult
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 先用 UUID 占位创建门店,得到真实 ID
|
||||
shop := model.Shop{
|
||||
Name: in.ShopName,
|
||||
Code: uuid.New().String(),
|
||||
Address: in.Address,
|
||||
Phone: in.Phone,
|
||||
ManagerName: in.ManagerName,
|
||||
Description: in.Description,
|
||||
}
|
||||
if err := tx.Create(&shop).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 用自增 ID 生成正式门店编码
|
||||
shop.Code = fmt.Sprintf("S%06d", shop.ID)
|
||||
if err := tx.Model(&shop).Update("code", shop.Code).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建管理员用户
|
||||
user := model.User{
|
||||
ShopID: shop.ID,
|
||||
Username: in.Username,
|
||||
PasswordHash: string(hash),
|
||||
RealName: in.ManagerName,
|
||||
Phone: in.Phone,
|
||||
Role: "admin",
|
||||
IsActive: true,
|
||||
}
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = RegisterResult{
|
||||
ShopCode: shop.Code,
|
||||
ShopName: shop.Name,
|
||||
Username: user.Username,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// HashPassword 生成 bcrypt 哈希
|
||||
func HashPassword(plain string) (string, error) {
|
||||
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
|
||||
|
||||
@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS `shops` (
|
||||
`address` VARCHAR(255) DEFAULT NULL,
|
||||
`phone` VARCHAR(30) DEFAULT NULL,
|
||||
`manager_name` VARCHAR(50) DEFAULT NULL COMMENT '负责人',
|
||||
`description` TEXT DEFAULT NULL COMMENT '店铺简介',
|
||||
`logo_url` VARCHAR(500) DEFAULT '' COMMENT '门店 logo URL',
|
||||
`business_license` VARCHAR(500) DEFAULT NULL COMMENT '营业执照照片URL',
|
||||
`shop_photos` JSON DEFAULT NULL COMMENT '门店照片URL数组',
|
||||
|
||||
@@ -53,6 +53,7 @@ func SetupTestDB() *gorm.DB {
|
||||
deleted_at DATETIME,
|
||||
name TEXT NOT NULL,
|
||||
code TEXT UNIQUE,
|
||||
description TEXT,
|
||||
address TEXT,
|
||||
phone TEXT,
|
||||
manager_name TEXT,
|
||||
|
||||
Reference in New Issue
Block a user