feat: 迁移商务账号层级和列表管理
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type AccountUsecase interface {
|
||||
List(context.Context, business.AccountFilter, ...business.ListOption) ([]*business.Account, int64, error)
|
||||
Find(context.Context, uint) (*business.Account, error)
|
||||
CreateChild(context.Context, *business.Account) (*business.Account, error)
|
||||
Update(context.Context, *business.AccountUpdate) error
|
||||
}
|
||||
type AccountService struct{ uc AccountUsecase }
|
||||
|
||||
func NewAccountService(uc *business.AccountUsecase) *AccountService { return &AccountService{uc: uc} }
|
||||
|
||||
func accountResponse(a *business.Account) *dto.BusinessAccountResponse {
|
||||
return &dto.BusinessAccountResponse{UserID: a.ID, Username: a.Username, NickName: a.NickName, RealName: a.RealName, Phone: a.Phone, Email: a.Email, QQNumber: a.QQNumber,
|
||||
Level: a.Level, ParentID: a.ParentID, IDPath: a.IDPath, ParentUserName: a.ParentUserName, Status: strconv.Itoa(a.Status), RejectReason: a.RejectReason, AuthorityID: a.AuthorityID,
|
||||
CreateTime: a.CreatedAt.Format("2006-01-02 15:04:05"), UpdateTime: a.UpdatedAt.Format("2006-01-02 15:04:05"), PromoteCount: a.PromoteCount, BindCount: a.BindCount}
|
||||
}
|
||||
|
||||
func (s *AccountService) List(ctx context.Context, req *dto.BusinessAccountQuery) (*dto.BusinessAccountPage, error) {
|
||||
if req == nil {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
page, size := req.Page, req.PageSize
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 10
|
||||
}
|
||||
if size > 500 {
|
||||
size = 500
|
||||
}
|
||||
if page > int(^uint(0)>>1)/size {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
filter := business.AccountFilter{UserID: req.UserID, Username: req.Username, NickName: req.NickName, RealName: req.RealName, Phone: req.Phone}
|
||||
for _, field := range []struct {
|
||||
raw string
|
||||
target **int
|
||||
}{{req.Level, &filter.Level}, {req.Status, &filter.Status}} {
|
||||
if field.raw == "" {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(field.raw)
|
||||
if err != nil || n < 0 || n > 3 {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
*field.target = &n
|
||||
}
|
||||
if req.BeginTime != "" && req.EndTime != "" {
|
||||
begin, err := time.ParseInLocation("2006-01-02 15:04:05", req.BeginTime, time.Local)
|
||||
if err != nil {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
end, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local)
|
||||
if err != nil || end.Before(begin) {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
filter.BeginTime, filter.EndTime = &begin, &end
|
||||
}
|
||||
rows, total, err := s.uc.List(ctx, filter, business.ListOffset((page-1)*size), business.ListLimit(size), business.ListOrderBy(req.OrderByColumn, strings.EqualFold(req.IsAsc, "desc") || req.IsAsc == "descending"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &dto.BusinessAccountPage{Rows: make([]*dto.BusinessAccountResponse, 0, len(rows)), Total: total}
|
||||
for _, a := range rows {
|
||||
out.Rows = append(out.Rows, accountResponse(a))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (s *AccountService) Find(ctx context.Context, id uint) (*dto.BusinessAccountResponse, error) {
|
||||
if id == 0 {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
a, err := s.uc.Find(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accountResponse(a), nil
|
||||
}
|
||||
|
||||
func validateAccount(req *dto.BusinessAccountRequest, create bool) (*int, error) {
|
||||
if req == nil {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
if create && (req.Username == nil || req.RealName == nil || strings.TrimSpace(*req.RealName) == "" || req.Password == "") {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
if req.Username != nil && (utf8.RuneCountInString(*req.Username) < 2 || utf8.RuneCountInString(*req.Username) > 30 || strings.TrimSpace(*req.Username) == "") {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
for _, field := range []struct {
|
||||
s *string
|
||||
max int
|
||||
}{{req.NickName, 30}, {req.RealName, 30}, {req.Phone, 11}, {req.Email, 50}, {req.QQNumber, 20}, {req.RejectReason, 200}} {
|
||||
if field.s != nil && (utf8.RuneCountInString(*field.s) > field.max || strings.ContainsAny(*field.s, "<>")) {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
}
|
||||
if req.Username != nil && strings.ContainsAny(*req.Username, "<>") {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
if create && (utf8.RuneCountInString(req.Password) < 5 || utf8.RuneCountInString(req.Password) > 50 || len(req.Password) > 72) {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
if req.Email != nil && *req.Email != "" {
|
||||
if _, err := mail.ParseAddress(*req.Email); err != nil {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
}
|
||||
if req.Status == nil {
|
||||
return nil, nil
|
||||
}
|
||||
status, err := strconv.Atoi(*req.Status)
|
||||
if err != nil || status < 0 || status > 3 {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func stringValue(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func (s *AccountService) Create(ctx context.Context, req *dto.BusinessAccountRequest) (*dto.BusinessAccountResponse, error) {
|
||||
status, err := validateAccount(req, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &business.Account{Username: stringValue(req.Username), NickName: stringValue(req.NickName), RealName: stringValue(req.RealName), Phone: stringValue(req.Phone),
|
||||
Email: stringValue(req.Email), QQNumber: stringValue(req.QQNumber), Password: req.Password, RejectReason: stringValue(req.RejectReason)}
|
||||
if status != nil {
|
||||
a.Status = *status
|
||||
}
|
||||
created, err := s.uc.CreateChild(ctx, a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accountResponse(created), nil
|
||||
}
|
||||
func (s *AccountService) Update(ctx context.Context, req *dto.BusinessAccountRequest) error {
|
||||
status, err := validateAccount(req, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.UserID == 0 {
|
||||
return business.ErrAccountInput
|
||||
}
|
||||
return s.uc.Update(ctx, &business.AccountUpdate{ID: req.UserID, Username: req.Username, NickName: req.NickName, RealName: req.RealName, Phone: req.Phone, Email: req.Email, QQNumber: req.QQNumber, Status: status, RejectReason: req.RejectReason})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type accountUsecaseFake struct {
|
||||
AccountUsecase
|
||||
created *business.Account
|
||||
filter business.AccountFilter
|
||||
options business.ListOptions
|
||||
}
|
||||
|
||||
func (f *accountUsecaseFake) CreateChild(_ context.Context, a *business.Account) (*business.Account, error) {
|
||||
f.created = a
|
||||
return a, nil
|
||||
}
|
||||
func (f *accountUsecaseFake) List(_ context.Context, filter business.AccountFilter, opts ...business.ListOption) ([]*business.Account, int64, error) {
|
||||
f.filter, f.options = filter, business.NewListOptions(opts...)
|
||||
return []*business.Account{}, 0, nil
|
||||
}
|
||||
|
||||
func TestAccountTransportIgnoresForgedOwnershipAndPreservesDefaultStatus(t *testing.T) {
|
||||
var req dto.BusinessAccountRequest
|
||||
if err := json.Unmarshal([]byte(`{"userId":999,"userName":"child","realName":"姓名","password":"abc123","level":1,"parentId":999,"roleIds":[888]}`), &req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := &accountUsecaseFake{}
|
||||
s := &AccountService{uc: f}
|
||||
value, err := s.Create(context.Background(), &req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.created.ID != 0 || f.created.ParentID != 0 || f.created.AuthorityID != 0 || f.created.Level != 0 || f.created.Status != 0 {
|
||||
t.Fatalf("传输层接受了身份注入: %+v", f.created)
|
||||
}
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := payload["password"]; ok {
|
||||
t.Fatal("响应不应包含密码")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountQueryEmptyFiltersAndPageBounds(t *testing.T) {
|
||||
f := &accountUsecaseFake{}
|
||||
s := &AccountService{uc: f}
|
||||
if _, err := s.List(context.Background(), &dto.BusinessAccountQuery{PageSize: 99999}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.filter.Level != nil || f.filter.Status != nil || f.options.Limit != 500 || f.options.Offset != 0 {
|
||||
t.Fatalf("缺省过滤或分页不正确: %+v %+v", f.filter, f.options)
|
||||
}
|
||||
if _, err := s.List(context.Background(), &dto.BusinessAccountQuery{Status: "0"}); err != nil || f.filter.Status == nil || *f.filter.Status != 0 {
|
||||
t.Fatalf("正常状态筛选未保留: %v", err)
|
||||
}
|
||||
if _, err := s.List(context.Background(), &dto.BusinessAccountQuery{Page: int(^uint(0) >> 1), PageSize: 500}); err != business.ErrAccountInput {
|
||||
t.Fatalf("溢出分页应拒绝: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package dto
|
||||
|
||||
type BusinessAccountQuery struct {
|
||||
Page int `form:"pageNum"`
|
||||
PageSize int `form:"pageSize"`
|
||||
UserID uint `form:"userId"`
|
||||
Username string `form:"userName"`
|
||||
NickName string `form:"nickName"`
|
||||
RealName string `form:"realName"`
|
||||
Phone string `form:"phoneNumber"`
|
||||
Level string `form:"level"`
|
||||
Status string `form:"status"`
|
||||
BeginTime string `form:"params[beginTime]"`
|
||||
EndTime string `form:"params[endTime]"`
|
||||
OrderByColumn string `form:"orderByColumn"`
|
||||
IsAsc string `form:"isAsc"`
|
||||
}
|
||||
|
||||
type BusinessAccountRequest struct {
|
||||
UserID uint `json:"userId"`
|
||||
Username *string `json:"userName"`
|
||||
NickName *string `json:"nickName"`
|
||||
Password string `json:"password"`
|
||||
RealName *string `json:"realName"`
|
||||
Phone *string `json:"phoneNumber"`
|
||||
Email *string `json:"email"`
|
||||
QQNumber *string `json:"qqNumber"`
|
||||
Status *string `json:"status"`
|
||||
RejectReason *string `json:"rejectReason"`
|
||||
}
|
||||
|
||||
type BusinessAccountResponse struct {
|
||||
UserID uint `json:"userId"`
|
||||
Username string `json:"userName"`
|
||||
NickName string `json:"nickName"`
|
||||
RealName string `json:"realName"`
|
||||
Phone string `json:"phoneNumber"`
|
||||
Email string `json:"email"`
|
||||
QQNumber string `json:"qqNumber"`
|
||||
Level int `json:"level"`
|
||||
ParentID uint `json:"parentId"`
|
||||
IDPath string `json:"idPath"`
|
||||
ParentUserName string `json:"parentUserName"`
|
||||
Status string `json:"status"`
|
||||
RejectReason string `json:"rejectReason"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
PromoteCount int64 `json:"promoteCount"`
|
||||
BindCount int64 `json:"bindCount"`
|
||||
}
|
||||
|
||||
type BusinessAccountPage struct {
|
||||
Rows []*BusinessAccountResponse `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// ProviderSet aggregates all service modules for Wire.
|
||||
var ProviderSet = wire.NewSet(
|
||||
businessservice.NewUserPreferenceService,
|
||||
businessservice.NewAccountService,
|
||||
systemservice.ProviderSet,
|
||||
paymentservice.ProviderSet,
|
||||
integrationservice.ProviderSet,
|
||||
|
||||
Reference in New Issue
Block a user