优化结构
This commit is contained in:
@@ -46,7 +46,7 @@ func waitForDataScopeAuditCount(t *testing.T, db *gorm.DB, want int64) {
|
||||
|
||||
func TestDataScopeAuditWriterFlushesBatch(t *testing.T) {
|
||||
db := openDataScopeAuditTestDB(t, "data-scope-audit-batch")
|
||||
d := &Data{gormDB: newReloadableDB(db, nil)}
|
||||
d := &Data{gormDB: newReloadableDB(db)}
|
||||
w := newDataScopeAuditWriterWithOptions(d, slog.New(slog.NewTextHandler(io.Discard, nil)), 8, 2, time.Hour)
|
||||
d.auditLog = w
|
||||
t.Cleanup(w.Close)
|
||||
@@ -59,7 +59,7 @@ func TestDataScopeAuditWriterFlushesBatch(t *testing.T) {
|
||||
func TestDataScopeAuditWriterUsesReloadedDatabase(t *testing.T) {
|
||||
first := openDataScopeAuditTestDB(t, "data-scope-audit-first")
|
||||
second := openDataScopeAuditTestDB(t, "data-scope-audit-second")
|
||||
d := &Data{gormDB: newReloadableDB(first, nil)}
|
||||
d := &Data{gormDB: newReloadableDB(first)}
|
||||
w := newDataScopeAuditWriterWithOptions(d, slog.New(slog.NewTextHandler(io.Discard, nil)), 8, 100, 10*time.Millisecond)
|
||||
d.auditLog = w
|
||||
t.Cleanup(w.Close)
|
||||
|
||||
@@ -16,11 +16,11 @@ import (
|
||||
|
||||
func TestGORMLoggerUsesApplicationCategories(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
appLogger, cleanup := logging.NewZapLogger(root, "application.log", logging.Options{Level: "info", Format: "json"})
|
||||
appLogger, control := logging.NewReloadableZapLogger(root, "application.log", logging.Options{Level: "info", Format: "json"})
|
||||
databaseLogger := gormkit.NewLogger(appLogger, logger.Info)
|
||||
databaseLogger.Trace(context.Background(), time.Now(), func() (string, int64) { return "SELECT 1", 1 }, nil)
|
||||
databaseLogger.Trace(context.Background(), time.Now(), func() (string, int64) { return "SELECT missing", 0 }, errors.New("database failure"))
|
||||
cleanup()
|
||||
control.Close()
|
||||
|
||||
date := time.Now().Format("2006-01-02")
|
||||
for _, name := range []string{"application.log", filepath.Join("sql", "application.log"), filepath.Join("error", "error.log")} {
|
||||
|
||||
@@ -247,7 +247,7 @@ func TestPersistRuntimeConfigReplacesActiveStorage(t *testing.T) {
|
||||
}
|
||||
d := &Data{
|
||||
runtime: config.NewStore(&config.Config{Data: &config.Data{}, Admin: currentAdmin}),
|
||||
gormDB: newReloadableDB(db, nil),
|
||||
gormDB: newReloadableDB(db),
|
||||
storage: currentStorage,
|
||||
}
|
||||
d.databaseReady.Store(true)
|
||||
|
||||
@@ -165,7 +165,7 @@ func (r *rollback) run() {
|
||||
|
||||
// newReloadableDB owns only the hot-swappable database handle. Domain-specific
|
||||
// callbacks are attached by Data when a pool becomes active.
|
||||
func newReloadableDB(db *gorm.DB, _ ...dataScopeAuditEnqueue) *reloadable[*gorm.DB] {
|
||||
func newReloadableDB(db *gorm.DB) *reloadable[*gorm.DB] {
|
||||
return newReloadable(db, closeGormDB)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.Order("id desc"), filter.Page, filter.PageSize, filter.PageSize > 0, func(po announcementPO) *system.Announcement {
|
||||
return listRows(db.Order("id desc"), filter.Page, filter.PageSize, rowQueryOptions{Paginate: filter.PageSize > 0}, func(po announcementPO) *system.Announcement {
|
||||
return announcementToBiz(po)
|
||||
})
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]system.UserOption
|
||||
}
|
||||
// The generated data-source endpoint is best effort: return collected
|
||||
// options even when the underlying scan reports an error.
|
||||
_ = r.data.DB().WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
|
||||
_ = r.data.DB().WithContext(ctx).Model(&userPO{}).Select("nick_name AS label, id AS value").Scan(&rows).Error
|
||||
items := make([]system.UserOption, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, system.UserOption{Label: row.Label, Value: row.Value})
|
||||
|
||||
@@ -249,11 +249,8 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
|
||||
}
|
||||
value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt
|
||||
value.DataScope, value.DefaultRouter = po.DataScope, po.DefaultRouter
|
||||
copyLinks := func(table string, destination any, columns map[string]any) error {
|
||||
return tx.Table(table).Where(columns).Find(destination).Error
|
||||
}
|
||||
var menus []authorityMenuPO
|
||||
if err := copyLinks("sys_authority_menus", &menus, map[string]any{"sys_authority_authority_id": sourceID}); err != nil {
|
||||
if err := tx.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", sourceID).Find(&menus).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if strict && len(menus) > 0 {
|
||||
@@ -316,7 +313,7 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
|
||||
}
|
||||
}
|
||||
var buttons []authorityButtonPO
|
||||
if err := copyLinks("sys_authority_btns", &buttons, map[string]any{"authority_id": sourceID}); err != nil {
|
||||
if err := tx.Model(&authorityButtonPO{}).Where("authority_id = ?", sourceID).Find(&buttons).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if strict && len(buttons) > 0 {
|
||||
|
||||
@@ -35,7 +35,7 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
|
||||
db = db.Where("target_table LIKE ?", "%"+q.TargetTable+"%")
|
||||
}
|
||||
}
|
||||
pos, total, err := queryRows[DataAccessLogPO](db.Order("id desc"), page, size, true, true)
|
||||
pos, total, err := queryRows[DataAccessLogPO](db.Order("id desc"), page, size, rowQueryOptions{Paginate: true, Required: true})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *syst
|
||||
db = db.Where("info LIKE ?", "%"+*q.Info+"%")
|
||||
}
|
||||
}
|
||||
return listRows(db.Order("created_at desc"), page, size, true, func(po errorRecordPO) *system.ErrorRecord {
|
||||
return listRows(db.Order("created_at desc"), page, size, rowQueryOptions{Paginate: true}, func(po errorRecordPO) *system.ErrorRecord {
|
||||
return errorFromPO(po)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,13 +10,18 @@ import (
|
||||
// count the filtered query, apply the bounded page, load POs, and convert them
|
||||
// at the data/biz boundary. Domain-specific filters and ordering stay in each
|
||||
// repository.
|
||||
func queryRows[PO any](db *gorm.DB, page, size int, paginate, required bool) ([]PO, int64, error) {
|
||||
type rowQueryOptions struct {
|
||||
Paginate bool
|
||||
Required bool
|
||||
}
|
||||
|
||||
func queryRows[PO any](db *gorm.DB, page, size int, options rowQueryOptions) ([]PO, int64, error) {
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if paginate {
|
||||
if required {
|
||||
if options.Paginate {
|
||||
if options.Required {
|
||||
db = pagination.ApplyRequired(db, page, size, 100)
|
||||
} else {
|
||||
db = pagination.Apply(db, page, size, 100)
|
||||
@@ -29,8 +34,8 @@ func queryRows[PO any](db *gorm.DB, page, size int, paginate, required bool) ([]
|
||||
return pos, total, nil
|
||||
}
|
||||
|
||||
func listRows[PO any, DO any](db *gorm.DB, page, size int, paginate bool, convert func(PO) *DO) ([]*DO, int64, error) {
|
||||
pos, total, err := queryRows[PO](db, page, size, paginate, false)
|
||||
func listRows[PO any, DO any](db *gorm.DB, page, size int, options rowQueryOptions, convert func(PO) *DO) ([]*DO, int64, error) {
|
||||
pos, total, err := queryRows[PO](db, page, size, options)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *syst
|
||||
db = db.Where("created_at <= ?", *q.EndCreatedAt)
|
||||
}
|
||||
}
|
||||
pos, total, err := queryRows[loginLogPO](db.Order("id desc"), page, size, true, true)
|
||||
pos, total, err := queryRows[loginLogPO](db.Order("id desc"), page, size, rowQueryOptions{Paginate: true, Required: true})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *
|
||||
db = db.Where("status = ?", q.Status)
|
||||
}
|
||||
}
|
||||
pos, total, err := queryRows[operationPO](db.Order("id desc"), page, size, true, true)
|
||||
pos, total, err := queryRows[operationPO](db.Order("id desc"), page, size, rowQueryOptions{Paginate: true, Required: true})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -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.Order("id desc"), page, size, true, func(po parameterPO) *system.SystemParameter {
|
||||
return listRows(db.Order("id desc"), page, size, rowQueryOptions{Paginate: true}, func(po parameterPO) *system.SystemParameter {
|
||||
return parameterFromPO(po)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,5 +85,5 @@ func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *system.Securit
|
||||
}
|
||||
|
||||
func (r *securityRepo) BackfillPasswordUpdatedAt(ctx context.Context, at time.Time) error {
|
||||
return r.data.DB().WithContext(ctx).Table("sys_users").Where("password_updated_at IS NULL").Update("password_updated_at", at).Error
|
||||
return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("password_updated_at IS NULL").Update("password_updated_at", at).Error
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*system.User, erro
|
||||
return nil, err
|
||||
}
|
||||
var authorityPOs []authorityPO
|
||||
if err := r.data.DB().WithContext(ctx).Table("sys_authorities").Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil {
|
||||
if err := r.data.DB().WithContext(ctx).Model(&authorityPO{}).Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorities := make([]system.Authority, 0, len(authorityPOs))
|
||||
@@ -68,7 +68,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*system.User, erro
|
||||
return nil, err
|
||||
}
|
||||
var departmentPOs []departmentPO
|
||||
if err := r.data.DB().WithContext(ctx).Table("sys_departments").Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil {
|
||||
if err := r.data.DB().WithContext(ctx).Model(&departmentPO{}).Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
departments := make([]system.Department, 0, len(departmentPOs))
|
||||
@@ -86,7 +86,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*system.User, erro
|
||||
}
|
||||
}
|
||||
var positionPOs []positionPO
|
||||
if err := r.data.DB().WithContext(ctx).Table("sys_positions").Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil {
|
||||
if err := r.data.DB().WithContext(ctx).Model(&positionPO{}).Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
positions := make([]system.Position, 0, len(positionPOs))
|
||||
@@ -238,7 +238,7 @@ func (r *userRepo) HasAuthorityMenu(ctx context.Context, authorityID uint, name
|
||||
|
||||
func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*system.Menu, error) {
|
||||
var pos []menuPO
|
||||
err := r.data.DB().WithContext(ctx).Table("sys_base_menus").
|
||||
err := r.data.DB().WithContext(ctx).Model(&menuPO{}).
|
||||
Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
|
||||
Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.deleted_at IS NULL", authorityID).
|
||||
Order("sys_base_menus.sort").Scan(&pos).Error
|
||||
@@ -266,7 +266,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*s
|
||||
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
|
||||
}
|
||||
var buttons []menuButtonPO
|
||||
if err := r.data.DB().WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
|
||||
if err := r.data.DB().WithContext(ctx).Model(&menuButtonPO{}).Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, button := range buttons {
|
||||
|
||||
@@ -6,42 +6,42 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Adapter is the provider boundary used by the payment repository. Provider
|
||||
// SDK types stay in this package and are normalized to biz/payment results.
|
||||
type Adapter = bizpayment.PaymentAdapter
|
||||
|
||||
type Factory struct{}
|
||||
|
||||
func NewFactory() *Factory { return &Factory{} }
|
||||
|
||||
type adapterFactory func() Adapter
|
||||
type adapterFactory func() bizpayment.PaymentAdapter
|
||||
|
||||
// adapterFactories is the single payment integration registration point.
|
||||
// Keep constructors zero-state: provider configuration belongs to each call,
|
||||
// so an adapter can never accidentally retain secrets or order data.
|
||||
var adapterFactories = map[string]adapterFactory{
|
||||
bizpayment.PaymentAlipay: func() Adapter { return &alipayAdapter{} },
|
||||
bizpayment.PaymentAlipayV3: func() Adapter { return &alipayV3Adapter{} },
|
||||
bizpayment.PaymentWechatV2: func() Adapter { return &wechatV2Adapter{} },
|
||||
bizpayment.PaymentWechatV3: func() Adapter { return &wechatV3Adapter{} },
|
||||
bizpayment.PaymentApple: func() Adapter { return &appleAdapter{} },
|
||||
bizpayment.PaymentDouyin: func() Adapter { return &douyinAdapter{} },
|
||||
bizpayment.PaymentQQ: func() Adapter { return &qqAdapter{} },
|
||||
bizpayment.PaymentAllinPay: func() Adapter { return &allinpayAdapter{} },
|
||||
bizpayment.PaymentLakala: func() Adapter { return &lakalaAdapter{} },
|
||||
bizpayment.PaymentPayPal: func() Adapter { return &paypalAdapter{} },
|
||||
bizpayment.PaymentSaobei: func() Adapter { return &saobeiAdapter{} },
|
||||
bizpayment.PaymentChinaums: func() Adapter { return newVendorAdapter(bizpayment.PaymentChinaums, vendorChinaums) },
|
||||
bizpayment.PaymentSFT: func() Adapter { return newVendorAdapter(bizpayment.PaymentSFT, vendorSFT) },
|
||||
bizpayment.PaymentSuperPay: func() Adapter { return newVendorAdapter(bizpayment.PaymentSuperPay, vendorSupperPay) },
|
||||
bizpayment.PaymentWechatGame: func() Adapter { return newVendorAdapter(bizpayment.PaymentWechatGame, vendorWechatGame) },
|
||||
bizpayment.PaymentDouyinGame: func() Adapter { return newVendorAdapter(bizpayment.PaymentDouyinGame, vendorDouyinGame) },
|
||||
bizpayment.PaymentAlipay: func() bizpayment.PaymentAdapter { return &alipayAdapter{} },
|
||||
bizpayment.PaymentAlipayV3: func() bizpayment.PaymentAdapter { return &alipayV3Adapter{} },
|
||||
bizpayment.PaymentWechatV2: func() bizpayment.PaymentAdapter { return &wechatV2Adapter{} },
|
||||
bizpayment.PaymentWechatV3: func() bizpayment.PaymentAdapter { return &wechatV3Adapter{} },
|
||||
bizpayment.PaymentApple: func() bizpayment.PaymentAdapter { return &appleAdapter{} },
|
||||
bizpayment.PaymentDouyin: func() bizpayment.PaymentAdapter { return &douyinAdapter{} },
|
||||
bizpayment.PaymentQQ: func() bizpayment.PaymentAdapter { return &qqAdapter{} },
|
||||
bizpayment.PaymentAllinPay: func() bizpayment.PaymentAdapter { return &allinpayAdapter{} },
|
||||
bizpayment.PaymentLakala: func() bizpayment.PaymentAdapter { return &lakalaAdapter{} },
|
||||
bizpayment.PaymentPayPal: func() bizpayment.PaymentAdapter { return &paypalAdapter{} },
|
||||
bizpayment.PaymentSaobei: func() bizpayment.PaymentAdapter { return &saobeiAdapter{} },
|
||||
bizpayment.PaymentChinaums: func() bizpayment.PaymentAdapter { return newVendorAdapter(bizpayment.PaymentChinaums, vendorChinaums) },
|
||||
bizpayment.PaymentSFT: func() bizpayment.PaymentAdapter { return newVendorAdapter(bizpayment.PaymentSFT, vendorSFT) },
|
||||
bizpayment.PaymentSuperPay: func() bizpayment.PaymentAdapter { return newVendorAdapter(bizpayment.PaymentSuperPay, vendorSupperPay) },
|
||||
bizpayment.PaymentWechatGame: func() bizpayment.PaymentAdapter {
|
||||
return newVendorAdapter(bizpayment.PaymentWechatGame, vendorWechatGame)
|
||||
},
|
||||
bizpayment.PaymentDouyinGame: func() bizpayment.PaymentAdapter {
|
||||
return newVendorAdapter(bizpayment.PaymentDouyinGame, vendorDouyinGame)
|
||||
},
|
||||
}
|
||||
|
||||
// New constructs the SDK-backed adapter for a configured provider. Provider
|
||||
// identifiers are normalized at this I/O boundary so direct callers get the
|
||||
// same behavior as the configuration and business layers.
|
||||
func New(provider string) (Adapter, error) {
|
||||
func (*Factory) New(provider string) (bizpayment.PaymentAdapter, error) {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
factory, ok := adapterFactories[provider]
|
||||
if !ok {
|
||||
@@ -49,7 +49,3 @@ func New(provider string) (Adapter, error) {
|
||||
}
|
||||
return factory(), nil
|
||||
}
|
||||
|
||||
func (*Factory) New(provider string) (bizpayment.PaymentAdapter, error) {
|
||||
return New(provider)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
)
|
||||
|
||||
func TestEverySupportedProviderHasAdapter(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
for _, provider := range bizpayment.SupportedPaymentProviders {
|
||||
t.Run(provider, func(t *testing.T) {
|
||||
adapter, err := New(provider)
|
||||
adapter, err := factory.New(provider)
|
||||
if err != nil {
|
||||
t.Fatalf("New(%q): %v", provider, err)
|
||||
}
|
||||
@@ -20,8 +21,9 @@ func TestEverySupportedProviderHasAdapter(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewNormalizesProviderIdentifier(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
for _, provider := range bizpayment.SupportedPaymentProviders {
|
||||
adapter, err := New(" " + provider + " ")
|
||||
adapter, err := factory.New(" " + provider + " ")
|
||||
if err != nil {
|
||||
t.Fatalf("New(%q): %v", provider, err)
|
||||
}
|
||||
@@ -32,7 +34,7 @@ func TestNewNormalizesProviderIdentifier(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewRejectsUnknownProvider(t *testing.T) {
|
||||
if _, err := New("unknown-provider"); err == nil {
|
||||
if _, err := NewFactory().New("unknown-provider"); err == nil {
|
||||
t.Fatal("unknown provider unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type vendorPaymentAdapter struct {
|
||||
profile vendorProfile
|
||||
}
|
||||
|
||||
func newVendorAdapter(provider string, profile vendorProfile) Adapter {
|
||||
func newVendorAdapter(provider string, profile vendorProfile) bizpayment.PaymentAdapter {
|
||||
return &vendorPaymentAdapter{provider: provider, profile: profile}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ func (w *DailyWriter) Write(value []byte) (int, error) {
|
||||
defer w.mu.Unlock()
|
||||
date := time.Now().Format("2006-01-02")
|
||||
if w.file == nil || w.date != date {
|
||||
// Retention is enforced again when a long-lived writer rolls to a new day.
|
||||
// Running this only on rotation avoids a directory scan for every write.
|
||||
w.removeExpired()
|
||||
if w.file != nil {
|
||||
_ = w.file.Close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDailyWriterCleansExpiredDirectoriesOnRotation(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writer := NewDailyWriter(root, "application.log", 1)
|
||||
oldDate := time.Now().AddDate(0, 0, -3).Format("2006-01-02")
|
||||
oldDir := filepath.Join(root, oldDate)
|
||||
if err := os.MkdirAll(oldDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writer.date = "2000-01-01"
|
||||
if _, err := writer.Write([]byte("rotated\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = writer.Close()
|
||||
if _, err := os.Stat(oldDir); !os.IsNotExist(err) {
|
||||
t.Fatalf("expired directory still exists: err=%v", err)
|
||||
}
|
||||
}
|
||||
@@ -460,7 +460,7 @@ func safeModuleName(value string) string {
|
||||
}, value)
|
||||
}
|
||||
|
||||
// NewZapLogger adapts a Zap core to the slog logger used by Kratos v3.
|
||||
// newZapHandler adapts a Zap core to the slog handler used by Kratos v3.
|
||||
// The file layout remains compatible with the administration log viewer.
|
||||
func newZapHandler(root, filename string, options Options, errorSink *errorSinkState) (slog.Handler, func()) {
|
||||
file := NewDailyWriter(root, filename, options.RetentionDay)
|
||||
@@ -538,8 +538,3 @@ func NewReloadableZapLogger(root, filename string, options Options, attrs ...any
|
||||
logger := kratoslog.NewLogger(handler, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...)
|
||||
return logger, control
|
||||
}
|
||||
|
||||
func NewZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, func()) {
|
||||
logger, control := NewReloadableZapLogger(root, filename, options, attrs...)
|
||||
return logger, control.Close
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
const (
|
||||
CodeSuccess = httpx.CodeSuccess
|
||||
CodeError = httpx.CodeError
|
||||
CodePasswordChangeRequired = httpx.CodePasswordChangeRequired
|
||||
CodePasswordChangeRequired = middleware.CodePasswordChangeRequired
|
||||
)
|
||||
|
||||
type Response = httpx.Response
|
||||
@@ -25,5 +25,7 @@ func OK(c *gin.Context) { httpx.OK(c) }
|
||||
func OKWithData(c *gin.Context, data any) { httpx.OKWithData(c, data) }
|
||||
func Fail(c *gin.Context, message string) { httpx.Fail(c, message) }
|
||||
func NoAuth(c *gin.Context, message string) { httpx.NoAuth(c, message) }
|
||||
func SetTokenCookie(c *gin.Context, value string, maxAge int) { httpx.SetTokenCookie(c, value, maxAge) }
|
||||
func Claims(c *gin.Context) *system.AuthClaims { return middleware.Claims(c) }
|
||||
func SetTokenCookie(c *gin.Context, value string, maxAge int) {
|
||||
httpx.SetCookie(c, "x-token", value, maxAge)
|
||||
}
|
||||
func Claims(c *gin.Context) *system.AuthClaims { return middleware.Claims(c) }
|
||||
|
||||
@@ -12,9 +12,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CodeSuccess = 0
|
||||
CodeError = 7
|
||||
CodePasswordChangeRequired = 10001
|
||||
CodeSuccess = 0
|
||||
CodeError = 7
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
@@ -55,12 +54,13 @@ func requestUsesHTTPS(request *http.Request) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(forwarded), "https")
|
||||
}
|
||||
|
||||
// SetTokenCookie is the shared transport-level cookie policy for the admin API.
|
||||
func SetTokenCookie(c *gin.Context, value string, maxAge int) {
|
||||
// SetCookie applies the transport-level cookie policy without knowing the
|
||||
// application's cookie names or authentication semantics.
|
||||
func SetCookie(c *gin.Context, name, value string, maxAge int) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
cookie := &http.Cookie{Name: "x-token", Value: value, Path: "/", MaxAge: maxAge, Secure: requestUsesHTTPS(c.Request), HttpOnly: true, SameSite: http.SameSiteStrictMode}
|
||||
cookie := &http.Cookie{Name: name, Value: value, Path: "/", MaxAge: maxAge, Secure: requestUsesHTTPS(c.Request), HttpOnly: true, SameSite: http.SameSiteStrictMode}
|
||||
if maxAge < 0 {
|
||||
cookie.Expires = time.Unix(1, 0)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package httpx
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -31,3 +32,15 @@ func TestResponseHelpers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCookieUsesRequestedName(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.GET("/cookie", func(c *gin.Context) { SetCookie(c, "example", "value", 60) })
|
||||
req := httptest.NewRequest("GET", "/cookie", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if got := w.Header().Get("Set-Cookie"); !strings.HasPrefix(got, "example=") {
|
||||
t.Fatalf("Set-Cookie = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
|
||||
const claimsKey = "admin_claims"
|
||||
|
||||
const CodePasswordChangeRequired = 10001
|
||||
|
||||
// tokenAuthTimeout bounds a shared token authentication flight. It replaces the
|
||||
// leader request's own deadline, which followers must not inherit.
|
||||
const tokenAuthTimeout = 10 * time.Second
|
||||
@@ -62,20 +64,20 @@ func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool)
|
||||
return auth.AuthenticateToken(ctx, token)
|
||||
})
|
||||
if err != nil {
|
||||
httpx.SetTokenCookie(c, "", -1)
|
||||
httpx.SetCookie(c, "x-token", "", -1)
|
||||
httpx.NoAuth(c, tokenErrorMessage(err))
|
||||
return false
|
||||
}
|
||||
authentication, ok := value.(*system.TokenAuthentication)
|
||||
if !ok || authentication == nil || authentication.Claims == nil {
|
||||
httpx.SetTokenCookie(c, "", -1)
|
||||
httpx.SetCookie(c, "x-token", "", -1)
|
||||
httpx.NoAuth(c, "无法处理此token")
|
||||
return false
|
||||
}
|
||||
if authentication.Refreshed != nil {
|
||||
c.Header("new-token", authentication.Refreshed.Value)
|
||||
c.Header("new-expires-at", strconv.FormatInt(authentication.Refreshed.ExpiresAt.Unix(), 10))
|
||||
httpx.SetTokenCookie(c, authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
|
||||
httpx.SetCookie(c, "x-token", authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
|
||||
}
|
||||
c.Set(claimsKey, authentication.Claims)
|
||||
return true
|
||||
@@ -141,6 +143,6 @@ func MustChangePassword() gin.HandlerFunc {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
|
||||
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user