回调
CI / changes (push) Successful in 36s
CI / ad (push) Successful in 23s
CI / admin (push) Successful in 19s
CI / bff (push) Failing after 1h33m11s
CI / chore (push) Successful in 5m20s
CI / equipment (push) Successful in 7m24s
CI / express (push) Successful in 29s
CI / product (push) Successful in 21s
CI / record (push) Successful in 18s
CI / sale (push) Successful in 16s
CI / task (push) Successful in 15s
CI / user (push) Successful in 18s
CI / wecom (push) Successful in 16s

This commit is contained in:
2026-09-04 17:52:57 +08:00
27 changed files with 679 additions and 168 deletions
-7
View File
@@ -1,7 +0,0 @@
Nacos:
Hosts:
- rnacos:8848
NamespaceId: test
Group: LONE_SERVICES
RegisterIP: chore
ConfigID: chore
-58
View File
@@ -1,58 +0,0 @@
[base]
login_out_time=43200 #api接口超时时间分
login_refresh_out_time=83200
name = "chore-service"
listenOn = "0.0.0.0:10103"
mode = "dev"
[log]
path = "logs"
serviceName = "chore-service"
mode = "file"
encoding = "plain"
level = "info"
keepDays = 7
maxSize = 50
maxBackups = 5
compress = false
[oss]
accessKeyId = "LTAI5t7U8KvxSiPE5auwQUuu"
accessKeySecret = "xjfgbQQRpL4TcIspNuClg6bYADa3pk"
roleArn = "acs:ram::1663896742753915:role/oss-upload"
roleSessionName = "oss-upload"
region = "oss-cn-hangzhou"
bucketName = "lone-images"
endpoint = "https://oss-accelerate.aliyuncs.com"
host = "https://images.ailuowan.com"
dir = "upload/"
expireSeconds = 3600
[mysql]
host = '39.106.171.204'
port = 33066
user = 'root'
password = 'MOLXRZNOU4Y4'
database = 'dms-chore'
charset = 'utf8mb4'
prefix = ''
debug = true
[mysql_read]
host = '39.106.171.204'
port = 33066
user = 'root'
password = 'MOLXRZNOU4Y4'
database = 'dms-chore'
charset = 'utf8mb4'
prefix = ''
[redis]
host = '39.106.171.204'
password = 'lLMLcuPpzSj'
port = 6379
db = 0
[encrypt]
data_key = "u2t9T3luZtoRfhBstkFN6TiIMW38BA8a"
[services]
product = "product-service"
sale = "sale-service"
+4 -2
View File
@@ -2,7 +2,7 @@
login_out_time=43200 #api接口超时时间分
login_refresh_out_time=83200
name = "sale-service"
listenOn = "0.0.0.0:10700"
listenOn = "0.0.0.0:10300"
mode = "dev"
[log]
path = "logs"
@@ -53,5 +53,7 @@
[encrypt]
data_key = "u2t9T3luZtoRfhBstkFN6TiIMW38BA8a"
[services]
chore = "chore-service"
chore = "chore-service"
user = "user-service"
-46
View File
@@ -1,46 +0,0 @@
[base]
port = 8060
name = "task-service"
listenOn = "0.0.0.0:10900"
mode = "dev"
[log]
path = "logs"
serviceName = "task-service"
mode = "file"
encoding = "plain"
level = "info"
keepDays = 7
maxSize = 50
maxBackups = 5
compress = false
# 测试的地址
[mysql]
host = '39.106.171.204'
port = 33066
user = 'root'
password = 'MOLXRZNOU4Y4'
database = 'dms-task'
charset = 'utf8mb4'
prefix = ''
debug = true
[mysql_read]
host = '39.106.171.204'
port = 33066
user = 'root'
password = 'MOLXRZNOU4Y4'
database = 'dms-task'
charset = 'utf8mb4'
prefix = ''
[redis]
host = '39.106.171.204'
password = 'lLMLcuPpzSj'
port = 6379
db = 1
[log-redis]
host = '39.106.171.204'
password = 'lLMLcuPpzSj'
port = 6379
db = 3
[services]
product = "product-service"
+19
View File
@@ -0,0 +1,19 @@
package config
import (
"lone-services/pkg/sms"
"lone-services/pkg/utils"
)
func NewSMS() (sms.SMS, error) {
return sms.New(sms.Config{
AccessKeyId: utils.GetConfigString("sms.accessKeyId"),
AccessKeySecret: utils.GetConfigString("sms.accessKeySecret"),
SignName: utils.GetConfigString("sms.signName"),
Endpoint: utils.GetConfigString("sms.endpoint"),
TemplateCode: utils.GetConfigString("sms.template.code"),
TemplatePwd: utils.GetConfigString("sms.template.password"),
Timeout: utils.GetConfigInt("sms.timeout"),
})
}
+13 -5
View File
@@ -13,15 +13,23 @@ const (
GrantTypePassword = "password"
GrantTypeSms = "sms"
// Type* 登录身份(写入 session.type / X-User-Type),与订单侧约定一致
TypeSale uint8 = 1 // 销售
TypeStore uint8 = 2 // 门店
TypeUser uint8 = 3 // 普通用户
SmsSceneLogin int32 = 1
SmsSceneResetPassword int32 = 2
SmsRateLimitSeconds = 60
SmsCodeLength = 6
TypeSale uint8 = 1
TypeStore uint8 = 2
TypeUser uint8 = 3
SubjectTypeSale = "sale"
SubjectTypeStore = "store"
SubjectTypeUser = "user"
// ExtraJsonEmpty MySQL JSON 列不能写空串,无扩展字段时用 {}
GenderMale uint8 = 1
GenderFemale uint8 = 2
GenderUnknown uint8 = 3
ExtraJsonEmpty = "{}"
)
+1 -1
View File
@@ -29,7 +29,7 @@ type UserCreate struct {
type UserListItem struct {
Id int64 `json:"id"`
Name string `json:"name"`
HeadPortrait string `json:"head_portrait"`
Avatar string `json:"avatar"`
Mobile string `json:"mobile"`
OriginMobile string `json:"origin_mobile"`
Gender uint8 `json:"gender"`
+16 -1
View File
@@ -68,6 +68,21 @@ func findCredential(credentialType, identifier string) (*dao.UserCredential, err
return &cred, nil
}
func userHasEnabledClient(userId int64, clientCode string) (bool, error) {
var row dao.UserClient
clientModel := model.UserClientModel{}.Init()
if err := clientModel.GetOne(modelbase.Params{
Eq: map[string]string{
"user_id": strconv.FormatInt(userId, utils.NumberTen),
"client_code": clientCode,
"status": strconv.Itoa(int(dao.StatusEnabled)),
},
}, &row); err != nil {
return false, err
}
return row.Id >= utils.NumberOne, nil
}
func loadEnabledUser(userId int64) (*dao.UserRow, error) {
var row dao.UserRow
userModel := model.UserModel{}.Init()
@@ -118,7 +133,7 @@ func resolveOrCreateWechatUser(ctx context.Context, db *gorm.DB, openid, unionid
if userId < utils.NumberOne {
userModel := model.UserModel{}.Init()
userModel.Base = userModel.Base.WithTX(tx)
add := dao.UserCreate{Status: dao.StatusEnabled}
add := dao.UserCreate{Status: dao.StatusEnabled, Gender: dao.GenderUnknown}
if err := userModel.Create(&add); err != nil {
return err
}
+125 -2
View File
@@ -3,11 +3,13 @@ package logic
import (
"context"
"lone-services/pkg/modelbase"
"lone-services/pkg/utils"
"lone-services/pkg/validate"
user "lone-services/rpc/user/pb"
userconfig "lone-services/services/user/internal/config"
"lone-services/services/user/internal/dao"
"lone-services/services/user/internal/model"
"lone-services/services/user/internal/svc"
"lone-services/services/user/validator"
@@ -39,9 +41,9 @@ func (l *LoginLogic) Login(in *user.LoginReq) (*user.Response, error) {
case dao.GrantTypeOpenid:
return l.loginByOpenid(req)
case dao.GrantTypePassword:
return outResponse(utils.ErrorParams, "密码登录暂未开放"), nil
return l.loginByPassword(req)
case dao.GrantTypeSms:
return outResponse(utils.ErrorParams, "验证码登录暂未开放"), nil
return l.loginBySms(req)
default:
return outResponse(utils.ErrorParams, "不支持的登录方式"), nil
}
@@ -91,3 +93,124 @@ func (l *LoginLogic) loginByOpenid(req validator.LoginValidator) (*user.Response
}
return okResponse(ret), nil
}
func (l *LoginLogic) loginByPassword(req validator.LoginValidator) (*user.Response, error) {
client, err := loadEnabledClient(req.ClientCode)
if err != nil {
l.Errorf("login load client: %v", err)
return failResponse(utils.Fail), nil
}
if !clientAllowsGrant(client.AllowedGrants, dao.GrantTypePassword) {
return outResponse(utils.ErrorParams, "该端不支持密码登录"), nil
}
if len(req.Mobile) != utils.NumberEleven {
return failResponse(utils.ErrorMobileError), nil
}
encryptMobile, encErr := utils.EncryptPhone(req.Mobile)
if encErr != nil {
l.Errorf("login encrypt mobile: %v", encErr)
return failResponse(utils.ErrorEncryptAesError), nil
}
cred, credErr := findCredential(dao.CredentialTypePassword, encryptMobile)
if credErr != nil {
l.Errorf("login find credential: %v", credErr)
return failResponse(utils.Fail), nil
}
if cred == nil {
return failResponse(utils.ErrorNotFund), nil
}
if cred.Status == dao.StatusDisabled {
return outResponse(utils.Fail, "登录凭证已禁用"), nil
}
if !utils.EqualsPassword(req.Password, cred.Secret) {
return failResponse(utils.ErrorPwdError), nil
}
hasClient, clientErr := userHasEnabledClient(cred.UserId, req.ClientCode)
if clientErr != nil {
l.Errorf("login check user client: %v", clientErr)
return failResponse(utils.Fail), nil
}
if !hasClient {
return outResponse(utils.ErrorParams, "该端未开通或已禁用"), nil
}
row, userErr := loadEnabledUser(cred.UserId)
if userErr != nil {
l.Errorf("login load user: %v", userErr)
return failResponse(utils.ErrorNotFund), nil
}
ret, tokenErr := issueLoginToken(l.ctx, l.svcCtx.JWT, l.svcCtx.SaleSvcName, row, client)
if tokenErr != nil {
l.Errorf("login issue token: %v", tokenErr)
return failResponse(utils.Fail), nil
}
return okResponse(ret), nil
}
func (l *LoginLogic) loginBySms(req validator.LoginValidator) (*user.Response, error) {
client, err := loadEnabledClient(req.ClientCode)
if err != nil {
l.Errorf("login load client: %v", err)
return failResponse(utils.Fail), nil
}
if !clientAllowsGrant(client.AllowedGrants, dao.GrantTypeSms) {
return outResponse(utils.ErrorParams, "该端不支持验证码登录"), nil
}
if len(req.Mobile) != utils.NumberEleven {
return failResponse(utils.ErrorMobileError), nil
}
if ok, verifyErr := verifyAndConsumeSmsCode(l.ctx, dao.SmsSceneLogin, req.Mobile, req.SmsCode); verifyErr != nil {
l.Errorf("login verify sms: %v", verifyErr)
return failResponse(utils.Fail), nil
} else if !ok {
return outResponse(utils.ErrorParams, "验证码错误或已过期"), nil
}
encryptMobile, encErr := utils.EncryptPhone(req.Mobile)
if encErr != nil {
l.Errorf("login encrypt mobile: %v", encErr)
return failResponse(utils.ErrorEncryptAesError), nil
}
var exist dao.UserRow
userModel := model.UserModel{}.Init()
if getErr := userModel.GetOne(modelbase.Params{
Eq: map[string]string{"mobile": encryptMobile},
}, &exist); getErr != nil {
l.Errorf("login find user by mobile: %v", getErr)
return failResponse(utils.Fail), nil
}
if exist.Id < utils.NumberOne {
return failResponse(utils.ErrorNotFund), nil
}
if exist.Status == dao.StatusDisabled {
return outResponse(utils.Fail, "用户已禁用"), nil
}
hasClient, clientErr := userHasEnabledClient(exist.Id, req.ClientCode)
if clientErr != nil {
l.Errorf("login check user client: %v", clientErr)
return failResponse(utils.Fail), nil
}
if !hasClient {
return outResponse(utils.ErrorParams, "该端未开通或已禁用"), nil
}
row, userErr := loadEnabledUser(exist.Id)
if userErr != nil {
l.Errorf("login load user: %v", userErr)
return failResponse(utils.ErrorNotFund), nil
}
ret, tokenErr := issueLoginToken(l.ctx, l.svcCtx.JWT, l.svcCtx.SaleSvcName, row, client)
if tokenErr != nil {
l.Errorf("login issue token: %v", tokenErr)
return failResponse(utils.Fail), nil
}
return okResponse(ret), nil
}
@@ -0,0 +1,141 @@
package logic
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"strconv"
"time"
"lone-services/pkg/redis"
"lone-services/pkg/sms"
"lone-services/pkg/utils"
"lone-services/pkg/validate"
user "lone-services/rpc/user/pb"
"lone-services/services/user/internal/dao"
"lone-services/services/user/internal/svc"
"lone-services/services/user/validator"
"github.com/zeromicro/go-zero/core/logx"
)
type SendSmsLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLogic {
return &SendSmsLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *SendSmsLogic) SendSms(in *user.SendSmsReq) (*user.Response, error) {
var req validator.SendSmsValidator
if msg := validate.ValidateFromProto(in, &req); msg != utils.StringEmpty {
return outResponse(utils.ErrorParams, msg), nil
}
if l.svcCtx.SMS == nil {
return outResponse(utils.Fail, "短信服务未配置"), nil
}
kind, ok := smsSceneKind(req.Scene)
if !ok {
return outResponse(utils.ErrorParams, "短信场景不正确"), nil
}
freqKey := smsFreqRedisKey(req.Scene, req.Mobile)
okSet, err := redis.Client.SetNX(l.ctx, freqKey, "1", time.Duration(dao.SmsRateLimitSeconds)*time.Second).Result()
if err != nil {
l.Errorf("sms freq setnx: %v", err)
return failResponse(utils.Fail), nil
}
if !okSet {
return outResponse(utils.ErrorParams, "发送过于频繁,请稍后再试"), nil
}
code, err := genDigitCode(dao.SmsCodeLength)
if err != nil {
_ = redis.Client.Del(l.ctx, freqKey).Err()
l.Errorf("sms gen code: %v", err)
return failResponse(utils.Fail), nil
}
if err := l.svcCtx.SMS.SendByKind(req.Mobile, kind, map[string]string{"code": code}); err != nil {
_ = redis.Client.Del(l.ctx, freqKey).Err()
l.Errorf("sms send: %v", err)
return outResponse(utils.Fail, "短信发送失败"), nil
}
ttl := l.svcCtx.SMS.Timeout()
if ttl <= utils.NumberZero {
ttl = 600
}
codeKey := smsCodeRedisKey(req.Scene, req.Mobile)
if err := redis.Client.Set(l.ctx, codeKey, code, time.Duration(ttl)*time.Second).Err(); err != nil {
l.Errorf("sms cache code: %v", err)
return failResponse(utils.Fail), nil
}
return okResponse(nil), nil
}
func smsSceneKind(scene int32) (sms.TemplateKind, bool) {
switch scene {
case dao.SmsSceneLogin:
return sms.TemplateCode, true
case dao.SmsSceneResetPassword:
return sms.TemplatePassword, true
default:
return "", false
}
}
func smsCodeRedisKey(scene int32, mobile string) string {
return utils.CodeKey + strconv.Itoa(int(scene)) + ":" + mobile
}
func smsFreqRedisKey(scene int32, mobile string) string {
return "sms:freq:" + strconv.Itoa(int(scene)) + ":" + mobile
}
func verifyAndConsumeSmsCode(ctx context.Context, scene int32, mobile, code string) (bool, error) {
if code == utils.StringEmpty {
return false, nil
}
codeKey := smsCodeRedisKey(scene, mobile)
cached, err := redis.Client.Get(ctx, codeKey).Result()
if err == redis.Nil {
return false, nil
}
if err != nil {
return false, err
}
if cached != code {
return false, nil
}
if delErr := redis.Client.Del(ctx, codeKey).Err(); delErr != nil {
return false, delErr
}
return true, nil
}
func genDigitCode(length int) (string, error) {
if length <= utils.NumberZero {
return utils.StringEmpty, fmt.Errorf("invalid code length")
}
out := make([]byte, length)
for i := range out {
n, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return utils.StringEmpty, err
}
out[i] = byte('0' + n.Int64())
}
return string(out), nil
}
+19 -4
View File
@@ -2,6 +2,7 @@ package logic
import (
"context"
"fmt"
"lone-services/pkg/modelbase"
"lone-services/pkg/utils"
@@ -49,10 +50,23 @@ func (l *UserItemsLogic) UserItems(in *user.UserItemsReq) (*user.Response, error
size = modelbase.DefaultSize
}
prefix := modelbase.Prefix()
existsSQL := fmt.Sprintf(`EXISTS (
SELECT 1 FROM %suser_client uc
INNER JOIN %sclient c ON c.code = uc.client_code
WHERE uc.user_id = %susers.id
AND uc.status = %d
AND c.status = %d
AND c.subject_type = ?
)`, prefix, prefix, prefix, dao.StatusEnabled, dao.StatusEnabled)
params := modelbase.Params{
Order: "id DESC",
Page: page,
Size: size,
Other: map[string]string{
existsSQL: dao.SubjectTypeUser,
},
}
if v.Mobile != utils.StringEmpty {
phone := utils.GetSearchPhone(v.Mobile)
@@ -73,15 +87,16 @@ func (l *UserItemsLogic) UserItems(in *user.UserItemsReq) (*user.Response, error
items := make([]dao.UserListItem, 0, len(list))
for _, row := range list {
headPortrait, _ := utils.BuildImageURL(row.Avatar).(string)
mobile := row.Mobile
if plain, dErr := utils.DecryptPhone(row.Mobile); dErr == nil {
mobile = utils.DecryptPhoneReplace(plain)
if len(row.Mobile) >= 72 {
if plain, dErr := utils.DecryptPhone(row.Mobile); dErr == nil {
mobile = utils.DecryptPhoneReplace(plain)
}
}
items = append(items, dao.UserListItem{
Id: row.Id,
Name: row.Name,
HeadPortrait: headPortrait,
Avatar: row.Avatar,
Mobile: mobile,
OriginMobile: row.Mobile,
Gender: row.Gender,
@@ -23,6 +23,11 @@ func NewUserServer(svcCtx *svc.ServiceContext) *UserServer {
}
}
func (s *UserServer) SendSms(ctx context.Context, in *user.SendSmsReq) (*user.Response, error) {
l := logic.NewSendSmsLogic(ctx, s.svcCtx)
return l.SendSms(in)
}
func (s *UserServer) Login(ctx context.Context, in *user.LoginReq) (*user.Response, error) {
l := logic.NewLoginLogic(ctx, s.svcCtx)
return l.Login(in)
@@ -1,6 +1,7 @@
package svc
import (
"lone-services/pkg/sms"
"lone-services/pkg/utils"
"lone-services/services/user/internal/config"
@@ -14,6 +15,7 @@ type ServiceContext struct {
Prefix string
JWT *config.JWTAuth
SaleSvcName string
SMS sms.SMS
}
func NewServiceContext(c config.Config, db *gorm.DB, jwtAuth *config.JWTAuth) *ServiceContext {
@@ -21,11 +23,18 @@ func NewServiceContext(c config.Config, db *gorm.DB, jwtAuth *config.JWTAuth) *S
if saleSvc == utils.StringEmpty {
logx.Error("config services.sale empty")
}
smsClient, err := config.NewSMS()
if err != nil {
logx.Errorf("sms init: %v", err)
}
return &ServiceContext{
Config: c,
DB: db,
Prefix: utils.GetConfigString("mysql.prefix"),
JWT: jwtAuth,
SaleSvcName: saleSvc,
SMS: smsClient,
}
}
+7
View File
@@ -21,6 +21,7 @@ type (
Response = user.Response
RevokeBizClientData = user.RevokeBizClientData
RevokeBizClientReq = user.RevokeBizClientReq
SendSmsReq = user.SendSmsReq
UpdateMobileData = user.UpdateMobileData
UpdateMobileReq = user.UpdateMobileReq
UserBriefItem = user.UserBriefItem
@@ -30,6 +31,7 @@ type (
UsersByIdsReq = user.UsersByIdsReq
User interface {
SendSms(ctx context.Context, in *SendSmsReq, opts ...grpc.CallOption) (*Response, error)
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*Response, error)
Info(ctx context.Context, in *InfoReq, opts ...grpc.CallOption) (*Response, error)
UserItems(ctx context.Context, in *UserItemsReq, opts ...grpc.CallOption) (*Response, error)
@@ -51,6 +53,11 @@ func NewUser(cli zrpc.Client) User {
}
}
func (m *defaultUser) SendSms(ctx context.Context, in *SendSmsReq, opts ...grpc.CallOption) (*Response, error) {
client := user.NewUserClient(m.cli.Conn())
return client.SendSms(ctx, in, opts...)
}
func (m *defaultUser) Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*Response, error) {
client := user.NewUserClient(m.cli.Conn())
return client.Login(ctx, in, opts...)
+14
View File
@@ -11,6 +11,20 @@ type LoginValidator struct {
SmsCode string `validate:"required_if=GrantType sms,omitempty,max=8"`
}
type SendSmsValidator struct {
Mobile string `validate:"required,len=11"`
Scene int32 `validate:"required,oneof=1 2"`
}
func (p SendSmsValidator) GetMessage() validate.ValidatorMessages {
return validate.ValidatorMessages{
"Mobile.required": "手机号不能为空",
"Mobile.len": "手机号格式错误",
"Scene.required": "短信场景不能为空",
"Scene.oneof": "短信场景不正确",
}
}
func (p LoginValidator) GetMessage() validate.ValidatorMessages {
return validate.ValidatorMessages{
"ClientCode.required": "端编码不能为空",