diff --git a/main.go b/main.go index 37628ab..3363cc3 100644 --- a/main.go +++ b/main.go @@ -6,12 +6,12 @@ import ( "electricity_bill_calc/global" "electricity_bill_calc/logger" "electricity_bill_calc/model" + "electricity_bill_calc/repository" "electricity_bill_calc/router" "electricity_bill_calc/service" "fmt" "time" - "github.com/shopspring/decimal" "go.uber.org/zap" ) @@ -43,52 +43,46 @@ func init() { } func intializeSingularity() error { - singularityExists, err := service.UserService.IsUserExists("000") + l := logger.Named("Init", "Singularity") + singularityExists, err := repository.UserRepository.IsUserExists("000") if err != nil { - return fmt.Errorf("singularity detect failed: %w", err) + l.Error("检测奇点账号失败。", zap.Error(err)) + return fmt.Errorf("检测奇点账号失败: %w", err) } if singularityExists { + l.Info("奇点账号已经存在,跳过剩余初始化步骤。") return nil } - singularity := &model.User{ - Id: "000", - Username: "singularity", - Type: 2, - Enabled: true, - } - singularityName := "Singularity" + singularityId := "000" singularityExpires, err := model.ParseDate("2099-12-31") if err != nil { - return fmt.Errorf("singularity expires time parse failed: %w", err) + l.Error("奇点用户账号过期时间解析失败。", zap.Error(err)) + return fmt.Errorf("奇点用户账号过期时间解析失败: %w", err) } - singularityDetail := &model.UserDetail{ - Name: &singularityName, - UnitServiceFee: decimal.Zero, - ServiceExpiration: singularityExpires, + singularity := &model.ManagementAccountCreationForm{ + Id: &singularityId, + Username: "singularity", + Name: "Singularity", + Type: 2, + Enabled: true, + Expires: singularityExpires, } - verifyCode, err := service.UserService.CreateUser(singularity, singularityDetail) + verifyCode, err := service.UserService.CreateUserAccount( + singularity.IntoUser(), + singularity.IntoUserDetail()) if err != nil { - return fmt.Errorf("singularity account failed to create: %w", err) + l.Error("创建奇点账号失败。", zap.Error(err)) + return fmt.Errorf("创建奇点账号失败: %w", err) } logger.Info( - fmt.Sprintf("Singularity account created, use %s as verify code to reset password.", verifyCode), - zap.String("account", "singularity"), - zap.String("verifyCode", verifyCode), + fmt.Sprintf("奇点账号已经完成创建, 首次登录需要使用验证码 [%s] 重置密码。", *verifyCode), + zap.String("账号名称", "singularity"), + zap.String("验证码", *verifyCode), ) return nil } -func DBConnectionKeepLive() { - for range time.Tick(30 * time.Second) { - ctx, cancel := global.TimeoutContext() - defer cancel() - err := global.DB.Ping(ctx) - if err != nil { - continue - } - } -} - +// 清理Redis缓存中的孤儿键。 func RedisOrphanCleanup() { cleanLogger := logger.Named("Cache").With(zap.String("function", "Cleanup")) for range time.Tick(2 * time.Minute) { @@ -102,8 +96,6 @@ func RedisOrphanCleanup() { } func main() { - // 本次停用检测的原因是:使用Ping来保持数据库链接看起来没有什么用处。 - // go DBConnectionKeepLive() go RedisOrphanCleanup() app := router.App() app.Listen(fmt.Sprintf(":%d", config.ServerSettings.HttpPort)) diff --git a/model/user.go b/model/user.go index 4895a82..8ba6787 100644 --- a/model/user.go +++ b/model/user.go @@ -23,6 +23,38 @@ type ManagementAccountCreationForm struct { Expires Date } +func (m ManagementAccountCreationForm) IntoUser() *User { + return &User{ + Id: *m.Id, + Username: m.Username, + Password: "", + ResetNeeded: false, + UserType: m.Type, + Enabled: m.Enabled, + CreatedAt: nil, + } +} + +func (m ManagementAccountCreationForm) IntoUserDetail() *UserDetail { + return &UserDetail{ + Id: *m.Id, + Name: &m.Name, + Abbr: nil, + Region: nil, + Address: nil, + Contact: m.Contact, + Phone: m.Phone, + UnitServiceFee: decimal.Zero, + ServiceExpiration: m.Expires, + CreatedAt: time.Now(), + CreatedBy: nil, + LastModifiedAt: time.Now(), + LastModifiedBy: nil, + DeletedAt: nil, + DeletedBy: nil, + } +} + type User struct { Id string Username string diff --git a/repository/user.go b/repository/user.go index c304e93..58c1598 100644 --- a/repository/user.go +++ b/repository/user.go @@ -5,6 +5,8 @@ import ( "electricity_bill_calc/global" "electricity_bill_calc/logger" "electricity_bill_calc/model" + "electricity_bill_calc/tools" + "electricity_bill_calc/tools/time" "fmt" "github.com/doug-martin/goqu/v9" @@ -15,10 +17,12 @@ import ( type _UserRepository struct { log *zap.Logger + ds goqu.DialectWrapper } var UserRepository = _UserRepository{ log: logger.Named("Repository", "User"), + ds: goqu.Dialect("postgres"), } // 使用用户名查询指定用户的基本信息 @@ -32,7 +36,7 @@ func (ur _UserRepository) FindUserByUsername(username string) (*model.User, erro defer cancel() var user = new(model.User) - sql, params, _ := goqu.From("user").Where(goqu.C("username").Eq(username)).Prepared(true).ToSQL() + sql, params, _ := ur.ds.From("user").Where(goqu.Ex{"username": username}).Prepared(true).ToSQL() if err := pgxscan.Get(ctx, global.DB, &user, sql, params...); err != nil { ur.log.Error("从数据库查询指定用户名的用户基本信息失败。", zap.String("username", username), zap.Error(err)) return nil, err @@ -52,7 +56,7 @@ func (ur _UserRepository) FindUserById(uid string) (*model.User, error) { defer cancel() var user = new(model.User) - sql, params, _ := goqu.From("user").Where(goqu.C("id").Eq(uid)).Prepared(true).ToSQL() + sql, params, _ := ur.ds.From("user").Where(goqu.Ex{"id": uid}).Prepared(true).ToSQL() if err := pgxscan.Get(ctx, global.DB, &user, sql, params...); err != nil { ur.log.Error("从数据库查询指定用户唯一编号的用户基本信息失败。", zap.String("user id", uid), zap.Error(err)) return nil, err @@ -72,7 +76,7 @@ func (ur _UserRepository) FindUserDetailById(uid string) (*model.UserDetail, err defer cancel() var user = new(model.UserDetail) - sql, params, _ := goqu.From("user_detail").Where(goqu.C("id").Eq(uid)).Prepared(true).ToSQL() + sql, params, _ := ur.ds.From("user_detail").Where(goqu.Ex{"id": uid}).Prepared(true).ToSQL() if err := pgxscan.Get(ctx, global.DB, &user, sql, params...); err != nil { ur.log.Error("从数据库查询指定用户唯一编号的用户详细信息失败。", zap.String("user id", uid), zap.Error(err)) return nil, err @@ -92,7 +96,7 @@ func (ur _UserRepository) FindUserInformation(uid string) (*model.UserWithDetail defer cancel() var user = new(model.UserWithDetail) - sql, params, _ := goqu. + sql, params, _ := ur.ds. From("user").As("u"). Join( goqu.T("user_detail").As("ud"), @@ -104,7 +108,7 @@ func (ur _UserRepository) FindUserInformation(uid string) (*model.UserWithDetail "ud.name", "ud.abbr", "ud.region", "ud.address", "ud.contact", "ud.phone", "ud.unit_service_fee", "ud.service_expiration", "ud.created_at", "ud.created_by", "ud.last_modified_at", "ud.last_modified_by"). - Where(goqu.C("u.id").Eq(uid)). + Where(goqu.Ex{"u.id": uid}). Prepared(true).ToSQL() if err := pgxscan.Get( ctx, global.DB, &user, sql, params...); err != nil { @@ -126,7 +130,7 @@ func (ur _UserRepository) IsUserExists(uid string) (bool, error) { defer cancel() var userCount int - sql, params, _ := goqu.From("user").Select(goqu.COUNT("*")).Where(goqu.C("id").Eq(uid)).Prepared(true).ToSQL() + sql, params, _ := ur.ds.From("user").Select(goqu.COUNT("*")).Where(goqu.Ex{"id": uid}).Prepared(true).ToSQL() if err := pgxscan.Get(ctx, global.DB, &userCount, sql, params...); err != nil { ur.log.Error("从数据库查询指定用户唯一编号的用户基本信息失败。", zap.String("user id", uid), zap.Error(err)) return false, err @@ -136,3 +140,60 @@ func (ur _UserRepository) IsUserExists(uid string) (bool, error) { } return userCount > 0, nil } + +// 创建一个新用户 +func (ur _UserRepository) CreateUser(user model.User, detail model.UserDetail, operator *string) (bool, error) { + ctx, cancel := global.TimeoutContext() + defer cancel() + tx, err := global.DB.Begin(ctx) + if err != nil { + ur.log.Error("启动数据库事务失败。", zap.Error(err)) + return false, err + } + + createdTime := time.Now() + userSql, userParams, _ := ur.ds. + Insert("user"). + Rows( + goqu.Record{ + "id": user.Id, "username": user.Username, "password": user.Password, + "reset_needed": user.ResetNeeded, "type": user.UserType, "enabled": user.Enabled, + "created_at": createdTime, + }, + ). + Prepared(true).ToSQL() + userResult, err := tx.Exec(ctx, userSql, userParams...) + if err != nil { + ur.log.Error("向数据库插入新用户基本信息失败。", zap.Error(err)) + tx.Rollback(ctx) + return false, err + } + userDetailSql, userDetailParams, _ := ur.ds. + Insert("user_detail"). + Rows( + goqu.Record{ + "id": user.Id, "name": detail.Name, "abbr": tools.PinyinAbbr(*detail.Name), "region": detail.Region, + "address": detail.Address, "contact": detail.Contact, "phone": detail.Phone, + "unit_service_fee": detail.UnitServiceFee, "service_expiration": detail.ServiceExpiration, + "created_at": createdTime, "created_by": operator, + "last_modified_at": createdTime, "last_modified_by": operator, + }, + ). + Prepared(true).ToSQL() + detailResult, err := tx.Exec(ctx, userDetailSql, userDetailParams...) + if err != nil { + ur.log.Error("向数据库插入新用户详细信息失败。", zap.Error(err)) + tx.Rollback(ctx) + return false, err + } + + err = tx.Commit(ctx) + if err != nil { + ur.log.Error("提交数据库事务失败。", zap.Error(err)) + tx.Rollback(ctx) + return false, err + } else { + cache.AbolishRelation("user") + } + return userResult.RowsAffected() > 0 && detailResult.RowsAffected() > 0, nil +} diff --git a/service/user.go b/service/user.go new file mode 100644 index 0000000..d528222 --- /dev/null +++ b/service/user.go @@ -0,0 +1,148 @@ +package service + +import ( + "electricity_bill_calc/cache" + "electricity_bill_calc/config" + "electricity_bill_calc/exceptions" + "electricity_bill_calc/logger" + "electricity_bill_calc/model" + "electricity_bill_calc/repository" + "electricity_bill_calc/tools" + "electricity_bill_calc/tools/serial" + "electricity_bill_calc/tools/time" + + "github.com/fufuok/utils" + "github.com/google/uuid" + "github.com/samber/lo" + "go.uber.org/zap" +) + +type _UserService struct { + log *zap.Logger +} + +var UserService = _UserService{ + log: logger.Named("Service", "User"), +} + +func matchUserPassword(controlCode, testCode string) bool { + hashedCode := utils.Sha512Hex(testCode) + return controlCode == hashedCode +} + +// 处理用户登录的通用过程。 +func (us _UserService) processUserLogin(username, password string, userType []int16) (*model.User, *model.UserDetail, error) { + us.log.Info("处理用户登录。", zap.String("username", username)) + user, err := repository.UserRepository.FindUserByUsername(username) + if err != nil { + us.log.Error("处理用户登录失败。", zap.String("username", username), zap.Error(err)) + return nil, nil, err + } + if user == nil { + us.log.Warn("处理用户登录失败,用户不存在。", zap.String("username", username)) + return nil, nil, exceptions.NewAuthenticationError(404, "用户不存在。") + } + if !lo.Contains(userType, user.UserType) { + us.log.Warn("处理用户登录失败,用户类型错误。", zap.String("username", username), zap.Int16s("user type", userType)) + return nil, nil, exceptions.NewAuthenticationError(400, "用户类型不正确。") + } + if !user.Enabled { + us.log.Warn("处理用户登录失败,用户已被禁用。", zap.String("username", username)) + return nil, nil, exceptions.NewAuthenticationError(403, "用户已被禁用。") + } + if user.ResetNeeded { + us.log.Warn("处理用户登录失败,用户需要重置密码。", zap.String("username", username)) + authErr := exceptions.NewAuthenticationError(401, "用户凭据已失效。") + authErr.NeedReset = true + return nil, nil, authErr + } + if !matchUserPassword(user.Password, password) { + us.log.Warn("处理用户登录失败,密码错误。", zap.String("username", username)) + return nil, nil, exceptions.NewAuthenticationError(402, "用户凭据不正确。") + } + userDetail, err := repository.UserRepository.FindUserDetailById(user.Id) + if err != nil { + us.log.Error("处理企业用户登录失败,查询用户详细信息失败。", zap.String("username", username), zap.Error(err)) + return nil, nil, err + } + if userDetail.ServiceExpiration.Before(time.Now()) { + us.log.Warn("处理企业用户登录失败,用户服务已过期。", zap.String("username", username)) + return nil, nil, exceptions.NewAuthenticationError(406, "用户服务期限已过。") + } + return user, userDetail, nil +} + +// 处理企业用户登录 +func (us _UserService) ProcessEnterpriseUserLogin(username, password string) (*model.Session, error) { + user, userDetail, err := us.processUserLogin(username, password, []int16{model.USER_TYPE_ENT}) + if err != nil { + us.log.Error("处理企业用户登录失败。", zap.String("username", username), zap.Error(err)) + return nil, err + } + token, _ := uuid.NewRandom() + userSession := &model.Session{ + Uid: user.Id, + Name: user.Username, + Type: user.UserType, + Token: token.String(), + ExpiresAt: time.Now().Add(config.ServiceSettings.MaxSessionLife), + } + if userDetail != nil && userDetail.Name != nil { + userSession.Name = *userDetail.Name + } + err = cache.CacheSession(userSession) + if err != nil { + us.log.Error("处理企业用户登录失败,缓存用户会话失败。", zap.String("username", username), zap.Error(err)) + return nil, err + } + return userSession, nil +} + +// 处理运维、监管用户登录 +func (us _UserService) ProcessManagementUserLogin(username, password string) (*model.Session, error) { + user, userDetail, err := us.processUserLogin(username, password, []int16{model.USER_TYPE_OPS, model.USER_TYPE_SUP}) + if err != nil { + us.log.Error("处理运维、监管用户登录失败。", zap.String("username", username), zap.Error(err)) + return nil, err + } + token, _ := uuid.NewRandom() + userSession := &model.Session{ + Uid: user.Id, + Name: user.Username, + Type: user.UserType, + Token: token.String(), + ExpiresAt: time.Now().Add(config.ServiceSettings.MaxSessionLife), + } + if userDetail != nil { + userSession.Name = *userDetail.Name + } + err = cache.CacheSession(userSession) + if err != nil { + us.log.Error("处理运维、监管用户登录失败,缓存用户会话失败。", zap.String("username", username), zap.Error(err)) + return nil, err + } + return userSession, nil +} + +// 创建用户账号的通用方法。 +func (us _UserService) CreateUserAccount(user *model.User, detail *model.UserDetail) (*string, error) { + if lo.IsEmpty(user.Id) { + var prefix string + if user.UserType == model.USER_TYPE_ENT { + prefix = "E" + } else { + prefix = "S" + } + user.Id = serial.GeneratePrefixedUniqueSerialString(prefix) + detail.Id = user.Id + } + verifyCode := tools.RandStr(10) + user.Password = utils.Sha512Hex(verifyCode) + user.ResetNeeded = true + res, err := repository.UserRepository.CreateUser(*user, *detail, nil) + if err != nil || !res { + us.log.Error("创建用户账号失败。", zap.String("username", user.Username), zap.Error(err)) + return nil, err + } + return &verifyCode, nil +}