gozero-商城之用户微服务构建
一:商城微服务简介
该商城主要包括的微服务有:购物车、首页、订单服务、支付服务、用户服务、商品服务,主要采用的是go-zero来实现
商城的思维导图如下
图片转自:https://bbs.csdn.net/topics/608056514
从以上思维导图可以看出,我们根据业务职能做如下微服务的划分:
商品服务(product) - 商品的添加、信息查询、库存管理等功能
购物车服务(cart) - 购物车的增删改查、收藏等功能
订单服务(order) - 生成订单,订单管理等功能
支付服务(pay) - 通过调用第三方支付实现支付等功能
账号服务(user) - 用户信息、实名认证、账号设置、地址管理等功能
首页服务(home) - 首页商品推荐、排行榜、限时开抢、banner等功能
每个服务都可以再分为 api 服务和 rpc 服务。
api 服务对外,可提供给 app 调用。
rpc 服务是对内的,可提供给内部 api 服务或者其他 rpc 服务调用。
整个项目的流程大致如下:
图片转自:https://bbs.csdn.net/topics/608056514
二:项目目录结构创建
二用户表设计及Model开发
CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`userIdentity` varchar(255) DEFAULT '' COMMENT '用户唯一标识',
`userName` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名',
`passWord` varchar(50) NOT NULL DEFAULT '' COMMENT '用户密码,MD5加密',
`userNick` varchar(100) DEFAULT '' COMMENT '用户昵称',
`userFace` varchar(255) DEFAULT '' COMMENT '用户头像地址',
`UserSex` tinyint(1) DEFAULT '0' COMMENT '用户性别:0男,1女,2保密',
`userEmail` varchar(255) DEFAULT '' COMMENT '用户邮箱',
`userPhone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
`loginAddress` varchar(255) DEFAULT '' COMMENT '用户登录IP地址',
`create_time` timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
`update_time` timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `userName` (`userName`),
UNIQUE KEY `userPhone` (`userPhone`),
KEY `updateTime` (`updateTime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
用户MODEL生成
把目录切换到user/model目录下,然后在终端运行如下命令
goctl model mysql ddl -src user.sql -d -dir .
运行完后在model目录会看到如下内容
注:在数据库中先创建好表
三:用户RPC接口生成
1.编写proto文件
在rpc目录下建立account.proto文件
syntax = "proto3";
package account;
option go_package = "./account";
message RegisterReq{
string UserName = 1;
string PassWord = 2;
string UserNick = 3;
string UserFace = 4;
int64 UserSex = 5;
string UserEmail = 6;
}
message LoginReq{
string UserName = 1;
string PassWord = 2;
}
message CommonResply{
int64 Code = 1;
string Message = 2;
string Data = 3;
}
message UserInfoReq{
string UserIdentity = 1;
}
service user{
rpc Register(RegisterReq) returns(CommonResply);
rpc Login(LoginReq) returns(CommonResply);
rpc UserInfo(UserInfoReq) returns (CommonResply);
}
这时候在这个目录下运行这个命令
goctl rpc protoc account.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. -style go-zero
就会生成相应的RPC 文件,生成后的目录如下
2.rpc配置
(1)修改rpc/etc下的account.yml增加相应的配置
(2)在internal目录下的config.go建立相应的配置
(3)在internal目录下的loginc目录编写相应接口的逻辑
登录接口的代码如下:
package logic
import (
"context"
"encoding/json"
"errors"
"microshop/comm"
"microshop/user/model"
"net/http"
"microshop/user/rpc/internal/svc"
"microshop/user/rpc/types/account"
"github.com/zeromicro/go-zero/core/logx"
)
type LoginLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *LoginLogic) Login(in *account.LoginReq) (*account.CommonResply, error) {
// todo: add your logic here and delete this line
//获取用户信息
userData, _ := l.svcCtx.UserModel.FindOneByUserName(l.ctx, in.UserName)
if userData == nil {
return nil, errors.New("用户未注册")
}
//密码解密
dePassword, dePassErr := comm.Decrypt(userData.PassWord, []byte(l.svcCtx.Config.SecretKey))
if dePassErr != nil {
return nil, dePassErr
}
if in.PassWord != dePassword {
return nil, errors.New("密码错误")
}
//获取客户端IP
ip, _ := comm.ExternalIp()
user := model.User{
Id: userData.Id,
UserIdentity: userData.UserIdentity,
UserName: userData.UserName,
PassWord: userData.PassWord,
UserNick: userData.UserNick,
UserFace: userData.UserFace,
UserSex: userData.UserSex,
UserEmail: userData.UserEmail,
UserPhone: userData.UserPhone,
LoginAddress: ip.String(),
}
cntErr := l.svcCtx.UserModel.Update(l.ctx, &user)
if cntErr != nil {
return nil, cntErr
}
userJsonData, jsonErr := json.Marshal(userData)
if jsonErr != nil {
return nil, jsonErr
}
return &account.CommonResply{
Code: http.StatusOK,
Message: "登录成功",
Data: string(userJsonData),
}, nil
}
注册的代码如下:
package logic
import (
"context"
"errors"
"github.com/google/uuid"
"microshop/comm"
"microshop/user/model"
"microshop/user/rpc/internal/svc"
"microshop/user/rpc/types/account"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *RegisterLogic) Register(in *account.RegisterReq) (*account.CommonResply, error) {
// todo: add your logic here and delete this line
userData, _ := l.svcCtx.UserModel.FindOneByUserPhone(l.ctx, in.UserName)
if userData != nil {
return nil, errors.New("用户名已注册")
}
if len(in.PassWord) < 6 || len(in.PassWord) > 16 {
return nil, errors.New("密码之能是6~16位")
}
if ok := comm.CheckPassword(in.PassWord); !ok {
return nil, errors.New("密码格式错误")
}
if ok := comm.CheckUsername(in.UserName); !ok {
return nil, errors.New("用户名格式错误")
}
enPassWord, enPassErr := comm.Encrypt(in.PassWord, []byte(l.svcCtx.Config.SecretKey))
if enPassErr != nil {
return nil, enPassErr
}
newUUID, _ := uuid.NewUUID()
user := model.User{
UserIdentity: newUUID.String(),
UserName: in.UserName,
PassWord: enPassWord,
UserNick: in.UserNick,
UserFace: in.UserFace,
UserSex: in.UserSex,
UserEmail: in.UserEmail,
UserPhone: "",
LoginAddress: "",
}
cnt, cntErr := l.svcCtx.UserModel.Insert(l.ctx, &user)
if cntErr != nil {
return nil, cntErr
}
rowsAffected, rowsErr := cnt.RowsAffected()
if rowsErr != nil {
return nil, rowsErr
}
if rowsAffected == 0 {
return nil, errors.New("注册失败")
}
return &account.CommonResply{
Code: 0,
Message: "注册成功",
}, nil
}
查找用户信息的代码如下:
package logic
import (
"context"
"encoding/json"
"microshop/comm/errorx"
"net/http"
"microshop/user/rpc/internal/svc"
"microshop/user/rpc/types/account"
"github.com/zeromicro/go-zero/core/logx"
)
type UserInfoLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserInfoLogic {
return &UserInfoLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *UserInfoLogic) UserInfo(in *account.UserInfoReq) (*account.CommonResply, error) {
// 获取用户信息
userData, _ := l.svcCtx.UserModel.FindOneByUserIdentity(l.ctx, in.UserIdentity)
if userData == nil {
return nil, errorx.NewDefaultError("找不到该用户")
}
userJsonData, JsonErr := json.Marshal(userData)
if JsonErr != nil {
return nil, errorx.NewDefaultError("数据转换失败")
}
return &account.CommonResply{
Code: http.StatusOK,
Message: "获取成功",
Data: string(userJsonData),
}, nil
}
由于FindOneByUserIdentity不是通过命令生成的,需要自已开发,具体代码如下,位置在user_model.go文件里面
func (m *defaultUserModel) FindOneByUserIdentity(ctx context.Context, userIdentity string) (*User, error) {
payOidKey := fmt.Sprintf("%s%v", cachePayOidPrefix, userIdentity)
var resp User
err := m.QueryRowCtx(ctx, &resp, payOidKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error {
query := fmt.Sprintf("select %s from %s where `userIdentity` = ? limit 1", userRows, m.table)
return conn.QueryRowCtx(ctx, v, query, userIdentity)
})
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
(4)启动RPC服务
启动之前先开启ETCD服务和redis服务,如下
进入rpc目录 运行 go run account.go,运行成功后如下
(5)测试RPC接口
至此用户的RPC服务已经构建完成
四:用户API服务
1.编写用户API接口
在user目录下的API目录下建立api的目录下建立user.api
syntax ="v1"
info (
title: "microShop/user.api"
author: "jobhandsome"
version: "1.0.0"
)
type (
//定义注册的请求体
RegisterReq {
UserName string `json:"userName"` //用户名
Password string `json:"password"` //密码
UserNick string `json:"userNick"` //用户昵称
UserFace string `json:"userFace"` //用户头像地址
UserSex int64 `json:"userSex"` //用户性别
UserEmail string `json:"userEmail"` //用户邮箱
}
//登录请求体
LoginReq {
UserName string `json:"userName"` //用户名
Password string `json:"password"` //密码
}
UserInfoReq struct{}
UserInfoReply {
Code int64 `json:"code"`
Message string `json:"message"`
Data *UserInfoItem `json:"data"`
}
UserInfoItem {
UserIdentity string `json:"userIdentity"` // 用户唯一表哦是
UserName string `json:"userName"` // 用户名
UserNick string `json:"userNick"` // 用户昵称
UserFace string `json:"userFace"` // 用户头像地址
UserSex int64 `json:"userSex"` // 用户性别:0男,1女,2保密
UserEmail string `json:"userEmail"` // 用户邮箱
UserPhone string `json:"userPhone"` // 用户手机号
}
CommonResply {
Code int64 `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
}
)
@server (
prefix: account
)
service user-api{
@doc(
summary:"用户注册"
)
// 定义 http.HandleFunc 转换的 go 文件名称及方法,每个接口都会跟一个 handler
@handler Register
post /register(RegisterReq) returns (CommonResply)
@doc(
summary:"用户登录"
)
@handler Login
post /login(LoginReq) returns(CommonResply)
}
@server(
jwt:Auth
)
service user-api{
@doc(
summary:"用户信息"
)
@handler userInfo
post /userinfo(UserInfoReq) returns (UserInfoReply)
}
2.生成API
进入到API目录下,运行如下命令
goctl api go -api user.api -dir . -style go-zero
运行中生成的目录如下:
3.API配置
(1)user-api.yaml的配置
Name: user-api
Host: 0.0.0.0
Port: 9001
SecretKey: C8xHnG6s
# mysql配置
Mysql:
Host: 127.0.0.1:3306
User: root
Pass: !!str 123456
Data: user
Charset: utf8mb4
# redis 配置
CacheRedis:
- Host: 127.0.0.1:6379
Pass:
Type: node
BizRedis:
Host: 127.0.0.1:6379
Pass:
Type: node
Auth:
AccessSecret: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
AccessExpire: 64800
(2)config.go的配置
(3)服务依赖配置
(4)API接口内部开发
登录的接口代码如下
import (
"context"
"encoding/json"
"github.com/golang-jwt/jwt/v4"
"microshop/user/model"
"microshop/user/rpc/user"
"time"
"microshop/user/api/internal/svc"
"microshop/user/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type LoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.CommonResply, err error) {
// todo: add your logic here and delete this line
cnt, cntErr := l.svcCtx.Rpc.Login(l.ctx, &user.LoginReq{
UserName: req.UserName,
PassWord: req.Password,
})
if cntErr != nil {
return nil, cntErr
}
var userData model.User
userErr := json.Unmarshal([]byte(cnt.Data), &userData)
if userErr != nil {
return nil, userErr
}
//jwt
payloads := make(map[string]any)
payloads["userIdentity"] = userData.UserIdentity
accessToken, tokenErr := l.GetToken(time.Now().Unix(), l.svcCtx.Config.Auth.AccessSecret, payloads, l.svcCtx.Config.Auth.AccessExpire)
if tokenErr != nil {
return nil, tokenErr
}
return &types.CommonResply{
Code: cnt.Code,
Message: cnt.Message,
Data: accessToken,
}, nil
}
func (l *LoginLogic) GetToken(iat int64, secretKey string, payloads map[string]any, seconds int64) (string, error) {
claims := make(jwt.MapClaims)
claims["expTime"] = iat + seconds
claims["iat"] = iat
for k, v := range payloads {
claims[k] = v
}
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
注册的代码如下:
package logic
import (
"context"
"microshop/user/rpc/user"
"microshop/user/api/internal/svc"
"microshop/user/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.CommonResply, err error) {
// todo: add your logic here and delete this line
cnt, cntErr := l.svcCtx.Rpc.Register(l.ctx, &user.RegisterReq{
UserName: req.UserName,
PassWord: req.Password,
UserNick: req.UserNick,
UserFace: req.UserFace,
UserSex: req.UserSex,
UserEmail: req.UserEmail,
})
if cntErr != nil {
return nil, cntErr
}
return &types.CommonResply{
Code: cnt.Code,
Message: cnt.Message,
}, nil
}
获取用户信息的接口如下
package logic
import (
"context"
"encoding/json"
"fmt"
"microshop/comm/errorx"
"microshop/user/rpc/types/account"
"net/http"
"microshop/user/api/internal/svc"
"microshop/user/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UserInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserInfoLogic {
return &UserInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UserInfoLogic) UserInfo(req *types.UserInfoReq) (resp *types.UserInfoReply, err error) {
// todo: add your logic here and delete this line
userIdentity := fmt.Sprintf("%v", l.ctx.Value("userIdentity"))
userData, userErr := l.svcCtx.Rpc.UserInfo(l.ctx, &account.UserInfoReq{UserIdentity: userIdentity})
if userErr != nil {
return nil, userErr
}
userInfoItem := types.UserInfoItem{}
userJsonErr := json.Unmarshal([]byte(userData.Data), &userInfoItem)
if userJsonErr != nil {
return nil, errorx.NewDefaultError("数据转换失败")
}
return &types.UserInfoReply{
Code: http.StatusOK,
Message: "获取成功",
Data: &userInfoItem,
}, nil
}
(5)启动服务
在user/api目录下运行 go run user.go,启动成功后如下图
(6)用POSTMAN测试
至此整个用户的微服务已经完成了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)