diff --git a/internal/biz/payment/payment.go b/internal/biz/payment/payment.go index 1895f64..99b1091 100644 --- a/internal/biz/payment/payment.go +++ b/internal/biz/payment/payment.go @@ -388,12 +388,8 @@ func NewPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentH } func NewConfiguredPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentHooks, sources *PaymentOrderSourceRegistry, fulfillments *PaymentFulfillmentRegistry, appLogger *slog.Logger) (*PaymentUsecase, error) { - if sources == nil || sources.Len() == 0 { - return nil, errors.New("支付业务订单来源未注册,无法启动支付服务") - } - if fulfillments == nil || fulfillments.Len() == 0 { - return nil, errors.New("支付发货 handler 未注册,无法启动支付服务") - } + // Business modules are optional in the template. Missing modules are + // reported when the corresponding payment operation is invoked. return NewPaymentUsecase(repo, orders, hooks, sources, fulfillments, appLogger), nil } @@ -924,8 +920,6 @@ func (uc *PaymentUsecase) refundWithOrder(ctx context.Context, provider, tradeNo message := "" if providerErr != nil { message = providerErr.Error() - } else if !accepted { - message = "支付平台未接受退款请求" } order, err = uc.orders.CompletePaymentRefundRequest(ctx, provider, tradeNo, token, accepted, message) if err != nil { diff --git a/internal/biz/system/audit.go b/internal/biz/system/audit.go index 8b18429..fccd1ce 100644 --- a/internal/biz/system/audit.go +++ b/internal/biz/system/audit.go @@ -100,7 +100,6 @@ type ErrorRecord struct { type AuditRecordRepo interface { RecordOperation(context.Context, *OperationRecord) error RecordLogin(context.Context, *LoginLog) error - RecordDataAccess(context.Context, *DataAccessLog) error CreateError(context.Context, *ErrorRecord) error } diff --git a/internal/biz/system/media_upload.go b/internal/biz/system/media_upload.go index ea7507d..6853e01 100644 --- a/internal/biz/system/media_upload.go +++ b/internal/biz/system/media_upload.go @@ -206,7 +206,10 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin return nil, value } chunks, err := uc.ListChunks(ctx, uploadID) - if err != nil || len(chunks) != session.ChunkTotal { + if err != nil { + return fail(err) + } + if len(chunks) != session.ChunkTotal { return fail(fmt.Errorf("分片不全: %d/%d", len(chunks), session.ChunkTotal)) } sort.Slice(chunks, func(i, j int) bool { return chunks[i].Index < chunks[j].Index }) diff --git a/internal/config/clone.go b/internal/config/clone.go index 59f59b9..0a856fc 100644 --- a/internal/config/clone.go +++ b/internal/config/clone.go @@ -31,6 +31,22 @@ func MergeRuntimeConfig(current, next *Config) *Config { } if merged.Data == nil { merged.Data = cloneData(current.Data) + } else if current.Data != nil { + if merged.Data.Database == nil { + merged.Data.Database = clonePtr(current.Data.Database) + } + if merged.Data.Redis == nil { + merged.Data.Redis = cloneRedis(current.Data.Redis) + } + if merged.Data.Mongo == nil { + merged.Data.Mongo = cloneMongo(current.Data.Mongo) + } + if merged.Data.DatabaseList == nil { + merged.Data.DatabaseList = cloneSlice(current.Data.DatabaseList, clonePtr) + } + if merged.Data.RedisList == nil { + merged.Data.RedisList = cloneSlice(current.Data.RedisList, cloneRedis) + } } if merged.Admin == nil { merged.Admin = cloneAdmin(current.Admin) diff --git a/internal/config/runtime_test.go b/internal/config/runtime_test.go index 44ec1db..34a208b 100644 --- a/internal/config/runtime_test.go +++ b/internal/config/runtime_test.go @@ -168,3 +168,12 @@ func TestMergeRuntimeConfigPreservesOmittedSections(t *testing.T) { t.Fatal("MergeRuntimeConfig leaked mutable storage state into current config") } } + +func TestMergeRuntimeConfigPreservesOmittedNestedDataSections(t *testing.T) { + current := &Config{Data: &Data{Database: &Database{Source: "current-dsn"}}} + next := &Config{Data: &Data{}} + merged := MergeRuntimeConfig(current, next) + if merged.Data == nil || merged.Data.Database == nil || merged.Data.Database.Source != "current-dsn" { + t.Fatalf("nested database section was not preserved: %#v", merged.Data) + } +} diff --git a/internal/data/integration/provider.go b/internal/data/integration/provider.go index c80c04c..ca301fc 100644 --- a/internal/data/integration/provider.go +++ b/internal/data/integration/provider.go @@ -3,6 +3,8 @@ package integration import ( "gorm.io/gorm" "kra/internal/integration/runtimeconfig" + + "github.com/google/wire" ) // Provider is the minimal database/runtime seam for integration configuration. @@ -10,3 +12,5 @@ type Provider interface { DB() *gorm.DB IntegrationRuntime() *runtimeconfig.Store } + +var ProviderSet = wire.NewSet(NewIntegrationConfigRepo, NewPaymentConfigReader) diff --git a/internal/data/integration/providers.go b/internal/data/integration/providers.go deleted file mode 100644 index c325c87..0000000 --- a/internal/data/integration/providers.go +++ /dev/null @@ -1,5 +0,0 @@ -package integration - -import "github.com/google/wire" - -var ProviderSet = wire.NewSet(NewIntegrationConfigRepo, NewPaymentConfigReader) diff --git a/internal/data/payment/payment.go b/internal/data/payment/payment.go index da82979..8264855 100644 --- a/internal/data/payment/payment.go +++ b/internal/data/payment/payment.go @@ -424,15 +424,15 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value))) switch provider { case bizpayment.PaymentAlipay, bizpayment.PaymentAlipayV3: - return !contains([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized) + return !paymentkit.ContainsFold([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized) case bizpayment.PaymentWechatV2: - return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay", "pay_code", "payment_code"}, normalized) + return !paymentkit.ContainsFold([]string{"micropay", "micro_pay", "barcode", "barcode_pay", "pay_code", "payment_code"}, normalized) case bizpayment.PaymentWechatV3: - return !contains([]string{"micropay", "micro_pay", "codepay", "code_pay", "barcode", "barcode_pay", "facepay", "face_pay"}, normalized) + return !paymentkit.ContainsFold([]string{"micropay", "micro_pay", "codepay", "code_pay", "barcode", "barcode_pay", "facepay", "face_pay"}, normalized) case bizpayment.PaymentQQ: - return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay"}, normalized) + return !paymentkit.ContainsFold([]string{"micropay", "micro_pay", "barcode", "barcode_pay"}, normalized) case bizpayment.PaymentLakala: - return !contains([]string{"retail", "retail_pay", "micropay", "barcode"}, normalized) + return !paymentkit.ContainsFold([]string{"retail", "retail_pay", "micropay", "barcode"}, normalized) default: return true } @@ -516,12 +516,3 @@ func paymentCallbackAck(provider string, values map[string]any, success bool) bi } return ack } - -func contains(values []string, value string) bool { - for _, item := range values { - if item == value { - return true - } - } - return false -} diff --git a/internal/data/payment/provider.go b/internal/data/payment/provider.go index 8e17764..9cdebd0 100644 --- a/internal/data/payment/provider.go +++ b/internal/data/payment/provider.go @@ -1,9 +1,16 @@ package payment -import "gorm.io/gorm" +import ( + "gorm.io/gorm" + + "github.com/google/wire" +) // Provider is the narrow persistence seam required by payment repositories. // Keeping it here lets payment remain an independent data module. type Provider interface { DB() *gorm.DB } + +// ProviderSet wires payment persistence repositories. +var ProviderSet = wire.NewSet(NewPaymentRepo, NewPaymentOrderRepo) diff --git a/internal/data/payment/providers.go b/internal/data/payment/providers.go deleted file mode 100644 index 194a1b9..0000000 --- a/internal/data/payment/providers.go +++ /dev/null @@ -1,6 +0,0 @@ -package payment - -import "github.com/google/wire" - -// ProviderSet wires payment persistence repositories. -var ProviderSet = wire.NewSet(NewPaymentRepo, NewPaymentOrderRepo) diff --git a/internal/data/system/announcement.go b/internal/data/system/announcement.go index 1714dd6..7df579b 100644 --- a/internal/data/system/announcement.go +++ b/internal/data/system/announcement.go @@ -71,7 +71,7 @@ func (r *announcementRepo) List(ctx context.Context, filter system.AnnouncementF if filter.StartCreatedAt != nil && filter.EndCreatedAt != nil { db = db.Where("created_at BETWEEN ? AND ?", filter.StartCreatedAt, filter.EndCreatedAt) } - return listRows(db, filter.Page, filter.PageSize, filter.PageSize > 0, func(po announcementPO) *system.Announcement { + return listRows(db.Order("id desc"), filter.Page, filter.PageSize, filter.PageSize > 0, func(po announcementPO) *system.Announcement { return announcementToBiz(po) }) } diff --git a/internal/data/system/data_access_log.go b/internal/data/system/data_access_log.go index 1c86316..a839662 100644 --- a/internal/data/system/data_access_log.go +++ b/internal/data/system/data_access_log.go @@ -21,12 +21,10 @@ type DataAccessLogPO struct { func (DataAccessLogPO) TableName() string { return "sys_data_access_logs" } -func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *system.DataAccessLog) error { - return r.data.DB().WithContext(ctx).Create(&DataAccessLogPO{EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}).Error -} func dataAccessFromPO(v DataAccessLogPO) *system.DataAccessLog { return &system.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail} } + func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *system.DataAccessLog) ([]*system.DataAccessLog, int64, error) { db := r.data.DB().WithContext(ctx).Model(&DataAccessLogPO{}) if q != nil { @@ -47,6 +45,7 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q * } return out, total, nil } + func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error { return r.data.DB().WithContext(ctx).Delete(&DataAccessLogPO{}, "id IN ?", ids).Error } diff --git a/internal/data/system/parameter.go b/internal/data/system/parameter.go index 2abaf82..ff7d3cc 100644 --- a/internal/data/system/parameter.go +++ b/internal/data/system/parameter.go @@ -70,7 +70,7 @@ func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *s db = db.Where(clause.Like{Column: clause.Column{Name: "key"}, Value: "%" + q.Key + "%"}) } } - return listRows(db, page, size, true, func(po parameterPO) *system.SystemParameter { + return listRows(db.Order("id desc"), page, size, true, func(po parameterPO) *system.SystemParameter { return parameterFromPO(po) }) } diff --git a/internal/data/system/provider.go b/internal/data/system/provider.go index b2e624e..0f31270 100644 --- a/internal/data/system/provider.go +++ b/internal/data/system/provider.go @@ -3,6 +3,7 @@ package system import ( "kra/internal/config" + "github.com/google/wire" "gorm.io/gorm" ) @@ -14,3 +15,28 @@ type Provider interface { DatabaseReady() bool Runtime() *config.Store } + +// ProviderSet wires system repositories and their runtime-backed adapters. +var ProviderSet = wire.NewSet( + NewRuntimeSettings, + NewTokenIssuer, + NewUserRepo, + NewAuthorityAccessRepo, + NewAPIRepo, + NewPermissionRepo, + NewMenuRepo, + NewDepartmentRepo, + NewPositionRepo, + NewDictionaryRepo, + NewParameterRepo, + NewAPITokenRepo, + NewSecurityRepo, + NewVersionRepo, + NewExportRepo, + NewAuditRepo, + NewAuditRecorderRepo, + NewLogFileRepo, + NewMediaRepo, + NewAnnouncementRepo, + NewMaintenanceRepo, +) diff --git a/internal/data/system/providers.go b/internal/data/system/providers.go deleted file mode 100644 index a5cc858..0000000 --- a/internal/data/system/providers.go +++ /dev/null @@ -1,28 +0,0 @@ -package system - -import "github.com/google/wire" - -// ProviderSet wires system repositories and their runtime-backed adapters. -var ProviderSet = wire.NewSet( - NewRuntimeSettings, - NewTokenIssuer, - NewUserRepo, - NewAuthorityAccessRepo, - NewAPIRepo, - NewPermissionRepo, - NewMenuRepo, - NewDepartmentRepo, - NewPositionRepo, - NewDictionaryRepo, - NewParameterRepo, - NewAPITokenRepo, - NewSecurityRepo, - NewVersionRepo, - NewExportRepo, - NewAuditRepo, - NewAuditRecorderRepo, - NewLogFileRepo, - NewMediaRepo, - NewAnnouncementRepo, - NewMaintenanceRepo, -) diff --git a/internal/data/system/version.go b/internal/data/system/version.go index 83d201b..572550d 100644 --- a/internal/data/system/version.go +++ b/internal/data/system/version.go @@ -59,6 +59,7 @@ func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, co if code != "" { db = db.Where("version_code = ?", code) } + db = db.Order("id desc") var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err diff --git a/internal/data/task/provider.go b/internal/data/task/provider.go index e967012..c643606 100644 --- a/internal/data/task/provider.go +++ b/internal/data/task/provider.go @@ -1,7 +1,13 @@ package task -import dataprovider "kra/internal/data/provider" +import ( + dataprovider "kra/internal/data/provider" + + "github.com/google/wire" +) // Provider is the minimal database seam required by the task tables. // Task persistence must not depend on the full data runtime or system repos. type Provider = dataprovider.Database + +var ProviderSet = wire.NewSet(NewTaskRepo) diff --git a/internal/data/task/providers.go b/internal/data/task/providers.go deleted file mode 100644 index 7265f59..0000000 --- a/internal/data/task/providers.go +++ /dev/null @@ -1,5 +0,0 @@ -package task - -import "github.com/google/wire" - -var ProviderSet = wire.NewSet(NewTaskRepo) diff --git a/internal/logging/source.go b/internal/logging/source.go index 4c9dcd8..cd33fa8 100644 --- a/internal/logging/source.go +++ b/internal/logging/source.go @@ -70,7 +70,7 @@ func skipStackFile(filename string) bool { "/go/pkg/mod/", "/go.uber.org/", "/gorm.io/", - "/pkg/logging/", + "/internal/logging/", "/internal/transport/middleware/", "/internal/transport/router/", "/internal/server/handler/", diff --git a/internal/logging/zap.go b/internal/logging/zap.go index c499efd..8cdd8d5 100644 --- a/internal/logging/zap.go +++ b/internal/logging/zap.go @@ -26,7 +26,7 @@ type Options struct { } // ErrorEntry is the storage-neutral representation of an Error-level log. -// Keeping it in pkg/logging lets the log core report failures without taking a +// Keeping it in internal/logging lets the log core report failures without taking a // dependency on the application service or persistence layers. type ErrorEntry struct { Form, Info, Level, RequestID, TraceID string diff --git a/internal/paymentkit/status.go b/internal/paymentkit/status.go index eb7f5c8..93a03d5 100644 --- a/internal/paymentkit/status.go +++ b/internal/paymentkit/status.go @@ -13,7 +13,7 @@ func NormalizeStatus(value, fallback string) string { return "success" case "WAIT_BUYER_PAY", "USERPAYING", "NOTPAY", "PROCESSING", "PENDING", "CREATED", "CLIENT_PENDING", "ACCEPT", "ACCEPTED", "PAYING": return "pending" - case "CLOSED", "TRADE_CLOSED", "CANCELLED", "CANCELED", "REVOKED", "REFUND", "FAILED", "FAIL", "REJECTED", "DENIED", "DECLINED", "ERROR", "PAYERROR": + case "CLOSED", "TRADE_CLOSED", "CANCELLED", "CANCELED", "REVOKED", "FAILED", "FAIL", "REJECTED", "DENIED", "DECLINED", "ERROR", "PAYERROR": return "failed" default: return fallback @@ -22,7 +22,7 @@ func NormalizeStatus(value, fallback string) string { func NormalizeRefundStatus(value, fallback string) string { switch strings.ToUpper(strings.TrimSpace(value)) { - case "SUCCESS", "OK", "PAID", "REFUND_SUCCESS", "REFUNDED", "COMPLETED", "FINISHED": + case "SUCCESS", "OK", "PAID", "REFUND_SUCCESS", "REFUNDED", "COMPLETED", "FINISHED", "TRADE_SUCCESS": return "success" case "WAIT_BUYER_PAY", "USERPAYING", "NOTPAY", "PROCESSING", "PENDING", "CREATED", "ACCEPT", "ACCEPTED", "PAYING": return "pending" diff --git a/internal/paymentkit/status_test.go b/internal/paymentkit/status_test.go index 465b400..15c130d 100644 --- a/internal/paymentkit/status_test.go +++ b/internal/paymentkit/status_test.go @@ -24,3 +24,15 @@ func TestNormalizeStatusSeparatesRefundStates(t *testing.T) { t.Fatalf("refund status = %q, want success", got) } } + +func TestNormalizeStatusDoesNotTreatRefundAsPaymentFailure(t *testing.T) { + if got := NormalizeStatus("REFUND", "unknown"); got != "unknown" { + t.Fatalf("NormalizeStatus(REFUND) = %q, want fallback", got) + } +} + +func TestNormalizeRefundStatusAcceptsTradeSuccess(t *testing.T) { + if got := NormalizeRefundStatus("TRADE_SUCCESS", "unknown"); got != "success" { + t.Fatalf("NormalizeRefundStatus(TRADE_SUCCESS) = %q, want success", got) + } +} diff --git a/internal/server/httpx/response.go b/internal/server/httpx/response.go index c2522d9..3cc54b3 100644 --- a/internal/server/httpx/response.go +++ b/internal/server/httpx/response.go @@ -55,8 +55,7 @@ func requestUsesHTTPS(request *http.Request) bool { return strings.EqualFold(strings.TrimSpace(forwarded), "https") } -// SetTokenCookie is the shared transport-level cookie policy. It has no KRA -// business dependency, so handlers and middleware can use pkg/httpx directly. +// SetTokenCookie is the shared transport-level cookie policy for the admin API. func SetTokenCookie(c *gin.Context, value string, maxAge int) { if c == nil { return diff --git a/internal/service/dto/permission.go b/internal/service/dto/permission.go index 68cf6c5..e260fbc 100644 --- a/internal/service/dto/permission.go +++ b/internal/service/dto/permission.go @@ -1,9 +1,8 @@ package dto type GetAuthorityButtonsRequest struct { - MenuID uint `json:"menuID"` - AuthorityID uint `json:"authorityId"` - Selected []uint `json:"selected"` + MenuID uint `json:"menuID"` + AuthorityID uint `json:"authorityId"` } type SetAuthorityButtonsRequest struct { diff --git a/internal/service/payment/payment.go b/internal/service/payment/payment.go index ffc63d7..a87f6bf 100644 --- a/internal/service/payment/payment.go +++ b/internal/service/payment/payment.go @@ -16,21 +16,7 @@ func (s *PaymentService) Order(ctx context.Context, provider, tradeNo string) (* if err != nil { return nil, err } - return &dto.PaymentOrderResponse{ - ID: order.ID, - Provider: order.Provider, TradeNo: order.TradeNo, ProviderTradeNo: order.ProviderTradeNo, - BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject, - PaymentMode: order.PaymentMode, OriginalAmount: order.OriginalAmount, - Amount: order.Amount, PaidAmount: order.PaidAmount, PayerPaidAmount: order.PayerPaidAmount, - CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount, - DiscountAmount: order.DiscountAmount, ProviderDiscountAmount: order.ProviderDiscountAmount, - MerchantDiscountAmount: order.MerchantDiscountAmount, SettlementAmount: order.SettlementAmount, - Currency: order.Currency, PayerCurrency: order.PayerCurrency, AmountBreakdownKnown: order.AmountBreakdownKnown, - PaymentStatus: order.PaymentStatus, ProviderStatus: order.ProviderStatus, FulfillmentStatus: order.FulfillmentStatus, - RefundStatus: order.RefundStatus, RefundedAmount: order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount, RefundNo: order.RefundNo, LastError: order.LastError, - CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, PaidAt: order.PaidAt, - FulfilledAt: order.FulfilledAt, RefundedAt: order.RefundedAt, - }, nil + return paymentOrderResponse(order) } func (s *PaymentService) Orders(ctx context.Context, req *dto.PaymentOrderListRequest) ([]*dto.PaymentOrderResponse, int64, int, int, error) { diff --git a/internal/service/system/security_session.go b/internal/service/system/security_session.go index e84e8c8..a99ce2e 100644 --- a/internal/service/system/security_session.go +++ b/internal/service/system/security_session.go @@ -14,34 +14,10 @@ func NewSecurityService(uc *system.SecurityUsecase) *SecurityService { return &SecurityService{uc: uc} } -func (s *SecurityService) ActiveToken(ctx context.Context, username string) (string, error) { - return s.uc.ActiveToken(ctx, username) -} - -func (s *SecurityService) LoginLocked(ctx context.Context, username string) (bool, error) { - return s.uc.LoginLocked(ctx, username) -} - -func (s *SecurityService) IncrementLoginFailure(ctx context.Context, username string, expiration time.Duration) (int64, error) { - return s.uc.IncrementLoginFailure(ctx, username, expiration) -} - -func (s *SecurityService) LockLogin(ctx context.Context, username string, expiration time.Duration) error { - return s.uc.LockLogin(ctx, username, expiration) -} - -func (s *SecurityService) ClearLoginState(ctx context.Context, username string) { - s.uc.ClearLoginState(ctx, username) -} - func (s *SecurityService) EnsureLoginIPCounter(ctx context.Context, ip string, expiration time.Duration) (int, error) { return s.uc.EnsureLoginIPCounter(ctx, ip, expiration) } -func (s *SecurityService) IncrementLoginIP(ctx context.Context, ip string, expiration time.Duration) (int64, error) { - return s.uc.IncrementLoginIP(ctx, ip, expiration) -} - func (s *SecurityService) IncrementRateLimit(ctx context.Context, key string, expiration time.Duration) (int64, error) { return s.uc.IncrementRateLimit(ctx, key, expiration) } @@ -58,12 +34,6 @@ func (s *SecurityService) DeleteCaptcha(ctx context.Context, id string) error { return s.uc.DeleteCaptcha(ctx, id) } -func (s *SecurityService) UseMultipoint() bool { return s.uc.UseMultipoint() } - func (s *SecurityService) CaptchaSettings() system.CaptchaSettings { return s.uc.CaptchaRuntimeSettings() } - -func (s *SecurityService) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error { - return s.uc.RotateActiveToken(ctx, username, oldToken, newToken, expiration) -}