forked from free-lancers/electricity_bill_calc_service
		
	enhance(user):完成可运行的程序基本结构以及用户基本查询功能。
This commit is contained in:
		
							
								
								
									
										58
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								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)) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										148
									
								
								service/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								service/user.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user