优化结构

This commit is contained in:
Yvan
2026-08-22 12:13:16 +08:00
parent d987244d0e
commit b27a66e9d3
200 changed files with 1794 additions and 1715 deletions
+10 -11
View File
@@ -3,12 +3,11 @@ package data
import (
"database/sql"
"errors"
"kra/internal/biz/system"
"log/slog"
"reflect"
"strings"
"kra/internal/biz"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
@@ -64,7 +63,7 @@ func skipDataScope(db *gorm.DB) bool {
return ok && value
}
func validDataScope(scope biz.DataScope) bool {
func validDataScope(scope system.DataScope) bool {
return scope.UserID != 0 && scope.AuthorityID != 0 && scope.Scope >= 1 && scope.Scope <= 5 && scope.All == (scope.Scope == 1)
}
@@ -79,10 +78,10 @@ func applyDataScope(operation string, enqueue dataScopeAuditEnqueue) func(*gorm.
if skipDataScope(db) {
return
}
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
scope, ok := system.DataScopeFromContext(db.Statement.Context)
if !ok {
slog.WarnContext(db.Statement.Context, "数据权限: 业务表访问无身份上下文, 已拒绝", "mod", "data-scope", "table", db.Statement.Table)
recordDataScopeEvent(db, enqueue, "no_identity", operation, "无身份上下文访问受控表, 已拒绝", biz.DataScope{})
recordDataScopeEvent(db, enqueue, "no_identity", operation, "无身份上下文访问受控表, 已拒绝", system.DataScope{})
_ = db.AddError(errDataScopeRequired)
return
}
@@ -116,7 +115,7 @@ func applyDataScope(operation string, enqueue dataScopeAuditEnqueue) func(*gorm.
}
}
func recordDataScopeEvent(db *gorm.DB, enqueue dataScopeAuditEnqueue, eventType, operation, detail string, scope biz.DataScope) {
func recordDataScopeEvent(db *gorm.DB, enqueue dataScopeAuditEnqueue, eventType, operation, detail string, scope system.DataScope) {
if enqueue == nil {
return
}
@@ -128,7 +127,7 @@ func auditBlockedWrite(operation string, enqueue dataScopeAuditEnqueue) func(*go
if _, applied := db.Statement.Clauses["data_scope:applied"]; !applied || db.Error != nil || db.RowsAffected != 0 {
return
}
if scope, ok := biz.DataScopeFromContext(db.Statement.Context); ok && !scope.All {
if scope, ok := system.DataScopeFromContext(db.Statement.Context); ok && !scope.All {
recordDataScopeEvent(db, enqueue, "blocked_write", operation, "数据范围过滤后写操作影响 0 行(疑似越权尝试)", scope)
}
}
@@ -139,9 +138,9 @@ func stampOwnership(enqueue dataScopeAuditEnqueue) func(*gorm.DB) {
if !isControlledTable(db) || skipDataScope(db) {
return
}
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
scope, ok := system.DataScopeFromContext(db.Statement.Context)
if !ok {
recordDataScopeEvent(db, enqueue, "no_identity", "create", "无身份上下文访问受控表, 已拒绝", biz.DataScope{})
recordDataScopeEvent(db, enqueue, "no_identity", "create", "无身份上下文访问受控表, 已拒绝", system.DataScope{})
_ = db.AddError(errDataScopeRequired)
return
}
@@ -164,7 +163,7 @@ func stampUpdatedBy(db *gorm.DB) {
if !isControlledTable(db) || stmt.SkipHooks || !hasScopeField(db, "updated_by") {
return
}
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
scope, ok := system.DataScopeFromContext(db.Statement.Context)
if !ok || scope.UserID == 0 {
return
}
@@ -212,7 +211,7 @@ func stampDeletedBy(db *gorm.DB) {
if _, customZero := deletedAt.TagSettings["ZEROVALUE"]; customZero {
return
}
scope, ok := biz.DataScopeFromContext(stmt.Context)
scope, ok := system.DataScopeFromContext(stmt.Context)
if !ok || scope.UserID == 0 {
return
}
+5 -6
View File
@@ -3,10 +3,9 @@ package data
import (
"context"
"errors"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"gorm.io/gorm"
)
@@ -53,12 +52,12 @@ func TestDataScopeCallbacksFailClosedAndAllowExplicitSystemBypass(t *testing.T)
t.Fatalf("query without data scope error = %v", err)
}
invalidCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 1, All: false})
invalidCtx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 1, All: false})
if err := db.WithContext(invalidCtx).Find(&rows).Error; !errors.Is(err, errInvalidDataScope) {
t.Fatalf("query with invalid data scope error = %v", err)
}
scopedCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, DepartmentIDs: []uint{10}})
scopedCtx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, DepartmentIDs: []uint{10}})
rows = nil
if err := db.WithContext(scopedCtx).Order("id").Find(&rows).Error; err != nil {
t.Fatal(err)
@@ -67,7 +66,7 @@ func TestDataScopeCallbacksFailClosedAndAllowExplicitSystemBypass(t *testing.T)
t.Fatalf("scoped query rows = %+v", rows)
}
emptyCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 5})
emptyCtx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 5})
rows = nil
if err := db.WithContext(emptyCtx).Find(&rows).Error; err != nil {
t.Fatal(err)
@@ -91,7 +90,7 @@ func TestDataScopeCreateRequiresIdentityAndStampsOwnership(t *testing.T) {
t.Fatalf("create without data scope error = %v", err)
}
ctx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, PrimaryDeptID: 10, DepartmentIDs: []uint{10}})
ctx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, PrimaryDeptID: 10, DepartmentIDs: []uint{10}})
created := dataScopeRecord{Name: "owned", DeptID: 999, CreatedBy: 999}
if err := db.WithContext(ctx).Create(&created).Error; err != nil {
t.Fatal(err)
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
"kra/internal/biz/system"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/integration/storage"
@@ -139,7 +139,7 @@ func (d *Data) IsInitialized(context.Context) (bool, error) {
// InitializeDatabase opens and activates the configured database. The callback
// is the application-level first-install hook; data owns only lifecycle and
// schema migration, while initialize owns system seed orchestration.
func (d *Data) InitializeDatabase(ctx context.Context, input *biz.DatabaseConfig, seed func(context.Context, *gorm.DB) error) error {
func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseConfig, seed func(context.Context, *gorm.DB) error) error {
config := &conf.Data_Database{}
if current := d.runtime.Data(); current != nil && current.Database != nil {
config = proto.Clone(current.Database).(*conf.Data_Database)
+5 -6
View File
@@ -3,8 +3,7 @@ package data
import (
"encoding/json"
"errors"
"kra/internal/biz"
"kra/internal/biz/system"
"gorm.io/gorm"
)
@@ -14,15 +13,15 @@ func ensureCommunicationIntegrationConfigs(db *gorm.DB) error {
kind string
provider string
}{
{kind: biz.IntegrationKindMQ, provider: "emqx"},
{kind: biz.IntegrationKindMQ, provider: "rabbitmq"},
{kind: biz.IntegrationKindWebSocket, provider: "melody"},
{kind: system.IntegrationKindMQ, provider: "emqx"},
{kind: system.IntegrationKindMQ, provider: "rabbitmq"},
{kind: system.IntegrationKindWebSocket, provider: "melody"},
}
for _, item := range defaults {
var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", item.kind, item.provider).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
values, marshalErr := json.Marshal(biz.DefaultIntegrationConfig(item.kind, item.provider))
values, marshalErr := json.Marshal(system.DefaultIntegrationConfig(item.kind, item.provider))
if marshalErr != nil {
return marshalErr
}
-25
View File
@@ -2,7 +2,6 @@ package payment
import (
"kra/pkg/database/migration"
platformmodule "kra/pkg/module"
"gorm.io/gorm"
)
@@ -15,27 +14,3 @@ func Migrations() []migration.Step {
{ID: "202608200004_payment_defaults", Migrate: ensurePaymentIntegrationConfigs},
}
}
// AdminSurface describes the payment-owned entries shown in the system
// administration UI. The system module persists these records because it owns
// the menu/API/policy tables.
func AdminSurface() platformmodule.Surface {
return platformmodule.Surface{
Menus: []platformmodule.Menu{
{Name: "paymentOrders", Path: "paymentOrders", ParentName: "extensions", Component: "view/systemTools/payment/orders.vue", Title: "支付订单", Icon: "wallet", Sort: 6},
{Name: "paymentConfig", Path: "paymentConfig", ParentName: "extensions", Component: "view/systemTools/payment/config.vue", Title: "支付配置", Icon: "credit-card", Sort: 7},
},
APIs: []platformmodule.API{
{Path: "/payment/orders", Method: "GET", Group: "支付", Description: "分页查询支付订单"},
{Path: "/payment/order", Method: "POST", Group: "支付", Description: "查询支付订单"},
{Path: "/payment/orders/:provider/:tradeNo", Method: "GET", Group: "支付", Description: "按路径查询支付订单"},
{Path: "/payment/create", Method: "POST", Group: "支付", Description: "创建支付订单"},
{Path: "/payment/query", Method: "POST", Group: "支付", Description: "同步支付订单状态"},
{Path: "/payment/refund", Method: "POST", Group: "支付", Description: "申请支付订单退款"},
{Path: "/payment/orders/:provider/:tradeNo/refund", Method: "POST", Group: "支付", Description: "按路径申请支付订单退款"},
{Path: "/payment/fulfill", Method: "POST", Group: "支付", Description: "重试支付订单发货"},
{Path: "/payment/orders/:provider/:tradeNo/fulfill", Method: "POST", Group: "支付", Description: "按路径重试支付订单发货"},
{Path: "/payment/providers/:provider/test", Method: "POST", Group: "支付", Description: "测试支付渠道"},
},
}
}
+54 -54
View File
@@ -7,12 +7,12 @@ import (
"encoding/json"
"errors"
"fmt"
"kra/internal/biz/system"
"net/url"
"strconv"
"strings"
"time"
"kra/internal/biz"
datapayment "kra/internal/integration/payment"
"github.com/google/uuid"
@@ -21,13 +21,13 @@ import (
type paymentRepo struct{ data Provider }
func NewPaymentRepo(data Provider) biz.PaymentRepo { return &paymentRepo{data: data} }
func NewPaymentRepo(data Provider) system.PaymentRepo { return &paymentRepo{data: data} }
func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
for _, provider := range biz.SupportedPaymentProviders {
for _, provider := range system.SupportedPaymentProviders {
var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error
defaults := biz.DefaultIntegrationConfig(integrationKindPayment, provider)
defaults := system.DefaultIntegrationConfig(integrationKindPayment, provider)
if errors.Is(err, gorm.ErrRecordNotFound) {
encoded, _ := json.Marshal(defaults)
if err := db.Create(&integrationConfigPO{Kind: integrationKindPayment, Provider: provider, Enabled: false, Config: string(encoded)}).Error; err != nil {
@@ -61,7 +61,7 @@ func (r *paymentRepo) row(ctx context.Context, provider string) (*integrationCon
var row integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, biz.ErrPaymentProviderNotFound
return nil, nil, system.ErrPaymentProviderNotFound
}
return nil, nil, err
}
@@ -84,11 +84,11 @@ func (r *paymentRepo) adapter(ctx context.Context, provider string) (datapayment
return adapter, values, err
}
func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.PaymentTestResult, error) {
func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*system.PaymentTestResult, error) {
started := time.Now()
test := &biz.PaymentTestResult{Provider: provider, TradeNo: "", Passed: false, Stages: []biz.PaymentTestStage{}}
test := &system.PaymentTestResult{Provider: provider, TradeNo: "", Passed: false, Stages: []system.PaymentTestStage{}}
add := func(name, status, message, tradeNo string, since time.Time) {
test.Stages = append(test.Stages, biz.PaymentTestStage{Name: name, Status: status, Message: message, TradeNo: tradeNo, Duration: time.Since(since).Milliseconds()})
test.Stages = append(test.Stages, system.PaymentTestStage{Name: name, Status: status, Message: message, TradeNo: tradeNo, Duration: time.Since(since).Milliseconds()})
}
values, err := r.testRow(ctx, provider)
if err != nil {
@@ -97,7 +97,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
}
test.Mode = strings.ToLower(strings.TrimSpace(text(values, "environment")))
configStart := time.Now()
if err = biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, provider, values); err != nil {
if err = system.ValidateIntegrationConfig(system.IntegrationKindPayment, provider, values); err != nil {
add("config", "failed", err.Error(), "", configStart)
return test, err
}
@@ -117,11 +117,11 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
orders := &paymentOrderRepo{data: r.data}
extra, _ := json.Marshal(req.Extra)
localStart := time.Now()
order, _, err := orders.CreatePaymentOrder(ctx, &biz.PaymentOrder{
order, _, err := orders.CreatePaymentOrder(ctx, &system.PaymentOrder{
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
Subject: req.Subject, PaymentMode: biz.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
Currency: req.Currency, PaymentStatus: biz.PaymentStatusInitialized, FulfillmentStatus: biz.FulfillmentStatusPending,
RefundStatus: biz.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
Subject: req.Subject, PaymentMode: system.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
Currency: req.Currency, PaymentStatus: system.PaymentStatusInitialized, FulfillmentStatus: system.FulfillmentStatusPending,
RefundStatus: system.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
})
if err != nil {
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
@@ -159,7 +159,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
if queryID == "" {
queryID = req.TradeNo
}
if provider == biz.PaymentApple {
if provider == system.PaymentApple {
queryID = strings.TrimSpace(text(values, "test_transaction_id"))
if queryID == "" {
err = errors.New("Apple 连通性测试需要配置 test_transaction_id(沙箱交易 ID)")
@@ -179,7 +179,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
add("query", "failed", err.Error(), req.TradeNo, queryStart)
return test, err
}
if provider != biz.PaymentApple {
if provider != system.PaymentApple {
if order, err = orders.ApplyPaymentResult(ctx, provider, req.TradeNo, paymentTestProviderUpdate(queried)); err != nil {
add("local_order", "failed", "回写测试查单结果失败: "+err.Error(), req.TradeNo, queryStart)
return test, err
@@ -193,9 +193,9 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
test.Result = queried
add("query", "passed", "测试订单查询成功,状态: "+queried.Status, req.TradeNo, queryStart)
if queried.Status != "success" || provider == biz.PaymentApple {
if queried.Status != "success" || provider == system.PaymentApple {
message := "订单尚未支付成功,已完成配置、下单和查单连通性测试;请在沙箱完成付款后重试"
if provider == biz.PaymentApple {
if provider == system.PaymentApple {
message = "Apple 退款由 App Store 管理,已完成配置、下单和交易查询测试"
}
add("refund", "skipped", message, req.TradeNo, time.Now())
@@ -208,7 +208,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
add("refund", "failed", beginErr.Error(), req.TradeNo, refundStart)
return test, beginErr
}
refund, refundErr := adapter.Refund(ctx, &biz.PaymentRefundRequest{Provider: provider, TradeNo: req.TradeNo, ProviderTradeNo: order.ProviderTradeNo, QueryID: order.QueryID, RefundNo: order.RefundNo, Amount: req.Amount, TotalAmount: req.Amount, Currency: req.Currency}, values)
refund, refundErr := adapter.Refund(ctx, &system.PaymentRefundRequest{Provider: provider, TradeNo: req.TradeNo, ProviderTradeNo: order.ProviderTradeNo, QueryID: order.QueryID, RefundNo: order.RefundNo, Amount: req.Amount, TotalAmount: req.Amount, Currency: req.Currency}, values)
if refundErr != nil {
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, refundErr)
add("refund", "failed", refundErr.Error(), req.TradeNo, refundStart)
@@ -230,17 +230,17 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
return test, nil
}
func paymentTestFingerprint(req *biz.PaymentRequest) string {
func paymentTestFingerprint(req *system.PaymentRequest) string {
raw, _ := json.Marshal(req)
hash := sha256.Sum256(raw)
return hex.EncodeToString(hash[:])
}
func paymentTestProviderUpdate(result *biz.PaymentResult) *biz.PaymentProviderUpdate {
func paymentTestProviderUpdate(result *system.PaymentResult) *system.PaymentProviderUpdate {
if result == nil {
return nil
}
return &biz.PaymentProviderUpdate{
return &system.PaymentProviderUpdate{
Status: result.Status, ProviderStatus: result.Status, ProviderTradeNo: result.ProviderTradeNo, QueryID: result.QueryID,
Amount: result.Amount, PayerPaidAmount: result.PayerPaidAmount, CashPaidAmount: result.CashPaidAmount,
PointPaidAmount: result.PointPaidAmount, DiscountAmount: result.DiscountAmount,
@@ -250,21 +250,21 @@ func paymentTestProviderUpdate(result *biz.PaymentResult) *biz.PaymentProviderUp
}
}
func validatePaymentTestResult(provider, tradeNo string, result *biz.PaymentResult) error {
func validatePaymentTestResult(provider, tradeNo string, result *system.PaymentResult) error {
if result == nil {
return errors.New("支付渠道响应为空")
}
if strings.TrimSpace(result.Provider) != provider {
return errors.New("支付渠道响应的 provider 不匹配")
}
if value := strings.TrimSpace(result.TradeNo); provider != biz.PaymentApple && value != "" && value != tradeNo {
if value := strings.TrimSpace(result.TradeNo); provider != system.PaymentApple && value != "" && value != tradeNo {
return errors.New("支付渠道响应的商户订单号不匹配")
}
return nil
}
func queryPaymentTest(ctx context.Context, adapter datapayment.Adapter, queryID string, values map[string]any) (*biz.PaymentResult, error) {
var result *biz.PaymentResult
func queryPaymentTest(ctx context.Context, adapter datapayment.Adapter, queryID string, values map[string]any) (*system.PaymentResult, error) {
var result *system.PaymentResult
var err error
for attempt := 0; attempt < 3; attempt++ {
result, err = adapter.Query(ctx, queryID, values)
@@ -300,7 +300,7 @@ func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]
var row integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrPaymentProviderNotFound
return nil, system.ErrPaymentProviderNotFound
}
return nil, err
}
@@ -311,17 +311,17 @@ func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]
return values, nil
}
func paymentTestRequest(provider string, values map[string]any) *biz.PaymentRequest {
func paymentTestRequest(provider string, values map[string]any) *system.PaymentRequest {
tradeNo := "kra-test-" + time.Now().UTC().Format("20060102150405.000000000")
amount := configuredInt64(values, "test_amount", 1)
if amount <= 0 {
amount = 1
}
req := &biz.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
req := &system.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
if req.Currency == "" {
req.Currency = "CNY"
}
if provider == biz.PaymentApple {
if provider == system.PaymentApple {
req.TradeNo = uuid.NewString()
req.Extra["product_id"] = firstAny(values, "product_id", "test_product_id")
}
@@ -351,10 +351,10 @@ func validatePaymentTestSettings(provider string, values map[string]any) error {
return fmt.Errorf("test_extra 必须是 JSON 对象: %w", err)
}
}
if provider == biz.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
if provider == system.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
return errors.New("Apple 测试需要 test_transaction_id(沙箱交易 ID)")
}
if provider == biz.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
if provider == system.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
return errors.New("Apple 测试需要 test_product_id(沙箱商品 ID)")
}
return nil
@@ -377,7 +377,7 @@ func testModeEnabled(values map[string]any) bool {
}
}
func (r *paymentRepo) Create(ctx context.Context, req *biz.PaymentRequest) (*biz.PaymentResult, error) {
func (r *paymentRepo) Create(ctx context.Context, req *system.PaymentRequest) (*system.PaymentResult, error) {
if req == nil {
return nil, errors.New("支付下单请求为空")
}
@@ -396,7 +396,7 @@ func (r *paymentRepo) Create(ctx context.Context, req *biz.PaymentRequest) (*biz
func paymentProviderRequiresNotifyURL(provider string) bool {
switch provider {
case biz.PaymentApple, biz.PaymentAllinPay, biz.PaymentSaobei, biz.PaymentPayPal:
case system.PaymentApple, system.PaymentAllinPay, system.PaymentSaobei, system.PaymentPayPal:
return false
default:
return true
@@ -413,15 +413,15 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
}
keys := []string{"method", "pay_method", "trade_type", "pay_type", "channel"}
switch provider {
case biz.PaymentAlipay, biz.PaymentAlipayV3:
case system.PaymentAlipay, system.PaymentAlipayV3:
keys = []string{"method", "pay_method", "trade_type", "channel"}
case biz.PaymentWechatV2:
case system.PaymentWechatV2:
keys = []string{"trade_type", "pay_type", "method", "pay_method", "channel"}
case biz.PaymentWechatV3:
case system.PaymentWechatV3:
keys = []string{"trade_type", "pay_type", "method"}
case biz.PaymentQQ:
case system.PaymentQQ:
keys = []string{"trade_type", "pay_type", "method", "pay_method"}
case biz.PaymentLakala:
case system.PaymentLakala:
keys = []string{"method", "pay_method", "trade_type"}
}
value := firstAny(extra, keys...)
@@ -430,28 +430,28 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
}
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
switch provider {
case biz.PaymentAlipay, biz.PaymentAlipayV3:
case system.PaymentAlipay, system.PaymentAlipayV3:
return !contains([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized)
case biz.PaymentWechatV2:
case system.PaymentWechatV2:
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay", "pay_code", "payment_code"}, normalized)
case biz.PaymentWechatV3:
case system.PaymentWechatV3:
return !contains([]string{"micropay", "micro_pay", "codepay", "code_pay", "barcode", "barcode_pay", "facepay", "face_pay"}, normalized)
case biz.PaymentQQ:
case system.PaymentQQ:
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay"}, normalized)
case biz.PaymentLakala:
case system.PaymentLakala:
return !contains([]string{"retail", "retail_pay", "micropay", "barcode"}, normalized)
default:
return true
}
}
func (r *paymentRepo) Query(ctx context.Context, provider, tradeNo string) (*biz.PaymentResult, error) {
func (r *paymentRepo) Query(ctx context.Context, provider, tradeNo string) (*system.PaymentResult, error) {
a, c, err := r.adapter(ctx, provider)
if err != nil {
return nil, err
}
return a.Query(ctx, tradeNo, c)
}
func (r *paymentRepo) Refund(ctx context.Context, req *biz.PaymentRefundRequest) (*biz.PaymentResult, error) {
func (r *paymentRepo) Refund(ctx context.Context, req *system.PaymentRefundRequest) (*system.PaymentResult, error) {
if req == nil {
return nil, errors.New("支付退款请求为空")
}
@@ -461,7 +461,7 @@ func (r *paymentRepo) Refund(ctx context.Context, req *biz.PaymentRefundRequest)
}
return a.Refund(ctx, req, c)
}
func (r *paymentRepo) HandleCallback(ctx context.Context, callback *biz.PaymentCallback) (*biz.PaymentResult, error) {
func (r *paymentRepo) HandleCallback(ctx context.Context, callback *system.PaymentCallback) (*system.PaymentResult, error) {
if callback == nil {
return nil, errors.New("支付回调为空")
}
@@ -471,23 +471,23 @@ func (r *paymentRepo) HandleCallback(ctx context.Context, callback *biz.PaymentC
}
result, err := a.Callback(ctx, callback, c)
if err != nil {
return nil, &biz.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
return nil, &system.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
}
if result == nil {
err = errors.New("支付回调解析结果为空")
return nil, &biz.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
return nil, &system.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
}
result.SuccessAck = paymentCallbackAck(callback.Provider, c, true)
result.FailureAck = paymentCallbackAck(callback.Provider, c, false)
if result.Provider != callback.Provider {
err = errors.New("支付回调渠道不匹配")
return nil, &biz.PaymentCallbackError{Cause: err, Ack: result.FailureAck}
return nil, &system.PaymentCallbackError{Cause: err, Ack: result.FailureAck}
}
result.EventID = paymentCallbackEventID(callback, result)
return result, nil
}
func paymentCallbackEventID(callback *biz.PaymentCallback, result *biz.PaymentResult) string {
func paymentCallbackEventID(callback *system.PaymentCallback, result *system.PaymentResult) string {
if result != nil {
if eventID := strings.TrimSpace(result.EventID); eventID != "" {
return eventID
@@ -504,8 +504,8 @@ func paymentCallbackEventID(callback *biz.PaymentCallback, result *biz.PaymentRe
return hex.EncodeToString(hash[:])
}
func paymentCallbackAck(provider string, values map[string]any, success bool) biz.PaymentCallbackAck {
ack := biz.DefaultPaymentCallbackAck(provider, success)
func paymentCallbackAck(provider string, values map[string]any, success bool) system.PaymentCallbackAck {
ack := system.DefaultPaymentCallbackAck(provider, success)
prefix := "callback_success_"
if !success {
prefix = "callback_failure_"
@@ -524,7 +524,7 @@ func paymentCallbackAck(provider string, values map[string]any, success bool) bi
return ack
}
func callbackFields(callback *biz.PaymentCallback) map[string]string {
func callbackFields(callback *system.PaymentCallback) map[string]string {
fields := map[string]string{}
for key, value := range callback.Query {
fields[key] = value
@@ -569,5 +569,5 @@ func contains(values []string, value string) bool {
}
func validatePaymentConfig(provider string, values map[string]any) error {
return biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, provider, values)
return system.ValidateIntegrationConfig(system.IntegrationKindPayment, provider, values)
}
+11 -12
View File
@@ -1,10 +1,9 @@
package payment
import (
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
)
func TestValidatePaymentConfigRequiresDouyinAppIDWhenEnabled(t *testing.T) {
@@ -12,18 +11,18 @@ func TestValidatePaymentConfigRequiresDouyinAppIDWhenEnabled(t *testing.T) {
"merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_serial_no": "platform-serial",
}
err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentDouyin, values)
err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentDouyin, values)
if err == nil || !strings.Contains(err.Error(), "app_id") {
t.Fatalf("missing app_id error = %v", err)
}
values["app_id"] = "douyin-app"
if err = biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentDouyin, values); err != nil {
if err = system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentDouyin, values); err != nil {
t.Fatalf("valid Douyin configuration rejected: %v", err)
}
}
func TestValidatePaymentConfigAcceptsProviderAliases(t *testing.T) {
err := validatePaymentConfig(biz.PaymentDouyin, map[string]any{
err := validatePaymentConfig(system.PaymentDouyin, map[string]any{
"app_id": "douyin-app", "merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_cert_serial": "platform-serial",
})
@@ -37,13 +36,13 @@ func TestValidatePaymentConfigProviderRules(t *testing.T) {
name, provider, want string
values map[string]any
}{
{"allinpay order type", biz.PaymentAllinPay, "reqsn", map[string]any{"cus_id": "customer", "app_id": "app", "private_key": "private-key", "public_key": "public-key", "query_order_type": "payinfo"}},
{"paypal webhook", biz.PaymentPayPal, "webhook_id", map[string]any{"client_id": "client-id", "client_secret": "client-secret"}},
{"wechat v2 refund cert", biz.PaymentWechatV2, "client_cert", map[string]any{"app_id": "app", "merchant_id": "merchant", "mch_key": "key"}},
{"allinpay order type", system.PaymentAllinPay, "reqsn", map[string]any{"cus_id": "customer", "app_id": "app", "private_key": "private-key", "public_key": "public-key", "query_order_type": "payinfo"}},
{"paypal webhook", system.PaymentPayPal, "webhook_id", map[string]any{"client_id": "client-id", "client_secret": "client-secret"}},
{"wechat v2 refund cert", system.PaymentWechatV2, "client_cert", map[string]any{"app_id": "app", "merchant_id": "merchant", "mch_key": "key"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, test.provider, test.values)
err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, test.provider, test.values)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want %q", err, test.want)
}
@@ -52,13 +51,13 @@ func TestValidatePaymentConfigProviderRules(t *testing.T) {
}
func TestValidatePaymentConfigGenericRequiresRuntimeFields(t *testing.T) {
values := biz.DefaultIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentChinaums)
if err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentChinaums, values); err == nil {
values := system.DefaultIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums)
if err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums, values); err == nil {
t.Fatal("empty generic payment config unexpectedly accepted")
}
values["app_id"], values["merchant_id"] = "app", "merchant"
values["create_url"], values["query_url"], values["refund_url"], values["app_key"] = "https://pay.test/create", "https://pay.test/query", "https://pay.test/refund", "secret"
if err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentChinaums, values); err == nil {
if err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums, values); err == nil {
t.Fatal("generic config with only identity/endpoints unexpectedly accepted")
}
}
+4 -5
View File
@@ -1,16 +1,15 @@
package payment
import (
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
)
func TestPaymentDefinitionsProvideNonEmptyDefaults(t *testing.T) {
definitions := biz.IntegrationDefinitions(biz.IntegrationKindPayment)
if len(definitions) != len(biz.SupportedPaymentProviders) {
t.Fatalf("payment definitions = %d, want %d", len(definitions), len(biz.SupportedPaymentProviders))
definitions := system.IntegrationDefinitions(system.IntegrationKindPayment)
if len(definitions) != len(system.SupportedPaymentProviders) {
t.Fatalf("payment definitions = %d, want %d", len(definitions), len(system.SupportedPaymentProviders))
}
for _, definition := range definitions {
if definition.Provider == "" || definition.Name == "" || len(definition.Fields) == 0 {
+82 -81
View File
@@ -4,14 +4,15 @@ import (
"context"
"encoding/json"
"errors"
"kra/internal/biz/system"
"strings"
"time"
"kra/pkg/database/pagination"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz"
"kra/pkg/database/pagination"
)
type paymentOrderPO struct {
@@ -67,11 +68,11 @@ func (paymentOrderPO) TableName() string { return "pay_orders" }
type paymentOrderRepo struct{ data Provider }
func NewPaymentOrderRepo(data Provider) biz.PaymentOrderRepo {
func NewPaymentOrderRepo(data Provider) system.PaymentOrderRepo {
return &paymentOrderRepo{data: data}
}
func newPaymentOrderPO(order *biz.PaymentOrder) (*paymentOrderPO, error) {
func newPaymentOrderPO(order *system.PaymentOrder) (*paymentOrderPO, error) {
if order == nil {
return nil, errors.New("支付订单为空")
}
@@ -84,16 +85,16 @@ func newPaymentOrderPO(order *biz.PaymentOrder) (*paymentOrderPO, error) {
ID: order.ID, TradeNo: order.TradeNo, Provider: order.Provider,
ProviderTradeNo: optionalString(order.ProviderTradeNo), QueryID: order.QueryID,
BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
PaymentMode: defaultString(order.PaymentMode, biz.PaymentModeExternal), OriginalAmount: order.OriginalAmount,
PaymentMode: defaultString(order.PaymentMode, system.PaymentModeExternal), 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: defaultString(order.PaymentStatus, biz.PaymentStatusInitialized),
PaymentStatus: defaultString(order.PaymentStatus, system.PaymentStatusInitialized),
ProviderStatus: order.ProviderStatus,
FulfillmentStatus: defaultString(order.FulfillmentStatus, biz.FulfillmentStatusPending),
RefundStatus: defaultString(order.RefundStatus, biz.RefundStatusNone),
FulfillmentStatus: defaultString(order.FulfillmentStatus, system.FulfillmentStatusPending),
RefundStatus: defaultString(order.RefundStatus, system.RefundStatusNone),
RefundedAmount: order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount, RefundNo: order.RefundNo,
ConfirmationID: order.ConfirmationID, RequestFingerprint: order.RequestFingerprint,
CreatePayload: createPayload, Extra: extra, LastEventID: order.LastEventID,
@@ -104,15 +105,15 @@ func newPaymentOrderPO(order *biz.PaymentOrder) (*paymentOrderPO, error) {
}, nil
}
func toBizPaymentOrder(po *paymentOrderPO) *biz.PaymentOrder {
func toBizPaymentOrder(po *paymentOrderPO) *system.PaymentOrder {
if po == nil {
return nil
}
return &biz.PaymentOrder{
return &system.PaymentOrder{
ID: po.ID, TradeNo: po.TradeNo, Provider: po.Provider,
ProviderTradeNo: dereferenceString(po.ProviderTradeNo), QueryID: po.QueryID,
BusinessType: po.BusinessType, BusinessID: po.BusinessID, Subject: po.Subject,
PaymentMode: defaultString(po.PaymentMode, biz.PaymentModeExternal), OriginalAmount: po.OriginalAmount,
PaymentMode: defaultString(po.PaymentMode, system.PaymentModeExternal), OriginalAmount: po.OriginalAmount,
Amount: po.Amount, PaidAmount: po.PaidAmount, PayerPaidAmount: po.PayerPaidAmount,
CashPaidAmount: po.CashPaidAmount, PointPaidAmount: po.PointPaidAmount, DiscountAmount: po.DiscountAmount,
ProviderDiscountAmount: po.ProviderDiscountAmount, MerchantDiscountAmount: po.MerchantDiscountAmount,
@@ -130,7 +131,7 @@ func toBizPaymentOrder(po *paymentOrderPO) *biz.PaymentOrder {
}
}
func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.PaymentOrder) (*biz.PaymentOrder, bool, error) {
func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *system.PaymentOrder) (*system.PaymentOrder, bool, error) {
po, err := newPaymentOrderPO(order)
if err != nil {
return nil, false, err
@@ -153,18 +154,18 @@ func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.Pa
return nil, false, err
}
func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*system.PaymentOrder, error) {
var po paymentOrderPO
if err := r.data.DB().WithContext(ctx).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrPaymentOrderNotFound
return nil, system.ErrPaymentOrderNotFound
}
return nil, err
}
return toBizPaymentOrder(&po), nil
}
func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter biz.PaymentOrderFilter) ([]*biz.PaymentOrder, int64, error) {
func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter system.PaymentOrderFilter) ([]*system.PaymentOrder, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&paymentOrderPO{})
if value := strings.TrimSpace(filter.Provider); value != "" {
db = db.Where("provider = ?", value)
@@ -192,14 +193,14 @@ func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize
if err := pagination.ApplyRequired(db.Order("id desc"), page, pageSize, 100).Find(&rows).Error; err != nil {
return nil, 0, err
}
items := make([]*biz.PaymentOrder, 0, len(rows))
items := make([]*system.PaymentOrder, 0, len(rows))
for i := range rows {
items = append(items, toBizPaymentOrder(&rows[i]))
}
return items, total, nil
}
func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *system.PaymentProviderUpdate) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if update == nil {
return errors.New("支付下单结果为空")
@@ -214,11 +215,11 @@ func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tr
po.LastEventID = trimTo(update.EventID, 128)
po.LastPayloadHash = trimTo(update.PayloadHash, 64)
status := normalizeOrderPaymentStatus(update.Status)
if status == biz.PaymentStatusPaid {
if status == system.PaymentStatusPaid {
// Provider create responses are never sufficient proof of payment.
status = biz.PaymentStatusPending
status = system.PaymentStatusPending
}
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded && status != "" {
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded && status != "" {
po.PaymentStatus = status
}
po.Version++
@@ -226,7 +227,7 @@ func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tr
})
}
func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tradeNo string, update *system.PaymentProviderUpdate) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if update == nil {
return errors.New("支付查单结果为空")
@@ -235,13 +236,13 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
return err
}
if update.Amount > 0 && update.Amount != po.Amount {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
if update.PayerPaidAmount < 0 || update.CashPaidAmount < 0 || update.PointPaidAmount < 0 || update.DiscountAmount < 0 || update.ProviderDiscountAmount < 0 || update.MerchantDiscountAmount < 0 || update.SettlementAmount < 0 {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
if update.Currency != "" && !strings.EqualFold(update.Currency, po.Currency) {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
po.ProviderStatus = trimTo(update.ProviderStatus, 64)
po.LastEventID = trimTo(update.EventID, 128)
@@ -258,22 +259,22 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
po.AmountBreakdownKnown = true
}
switch normalizeOrderPaymentStatus(update.Status) {
case biz.PaymentStatusPaid:
if po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
po.PaymentStatus = biz.PaymentStatusPaid
case system.PaymentStatusPaid:
if po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
po.PaymentStatus = system.PaymentStatusPaid
}
po.PaidAmount = po.Amount
if po.PaidAt == nil {
now := time.Now().UTC()
po.PaidAt = &now
}
case biz.PaymentStatusPending:
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
po.PaymentStatus = biz.PaymentStatusPending
case system.PaymentStatusPending:
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
po.PaymentStatus = system.PaymentStatusPending
}
case biz.PaymentStatusFailed:
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
po.PaymentStatus = biz.PaymentStatusFailed
case system.PaymentStatusFailed:
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
po.PaymentStatus = system.PaymentStatusFailed
}
}
po.Version++
@@ -281,27 +282,27 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
})
}
func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider, tradeNo string, lease time.Duration) (*biz.PaymentOrder, string, bool, error) {
func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider, tradeNo string, lease time.Duration) (*system.PaymentOrder, string, bool, error) {
var token string
var duplicate bool
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.FulfillmentStatus == biz.FulfillmentStatusSucceeded {
if po.FulfillmentStatus == system.FulfillmentStatusSucceeded {
duplicate = true
return nil
}
if po.PaymentStatus != biz.PaymentStatusPaid {
return biz.ErrPaymentOrderState
if po.PaymentStatus != system.PaymentStatusPaid {
return system.ErrPaymentOrderState
}
now := time.Now().UTC()
if po.FulfillmentStatus == biz.FulfillmentStatusProcessing && po.FulfillmentLeaseUntil != nil && po.FulfillmentLeaseUntil.After(now) {
return biz.ErrPaymentOrderBusy
if po.FulfillmentStatus == system.FulfillmentStatusProcessing && po.FulfillmentLeaseUntil != nil && po.FulfillmentLeaseUntil.After(now) {
return system.ErrPaymentOrderBusy
}
if lease <= 0 {
lease = 10 * time.Minute
}
token = uuid.NewString()
until := now.Add(lease)
po.FulfillmentStatus = biz.FulfillmentStatusProcessing
po.FulfillmentStatus = system.FulfillmentStatusProcessing
po.FulfillmentToken = token
po.FulfillmentLeaseUntil = &until
po.LastError = ""
@@ -311,56 +312,56 @@ func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider
return order, token, duplicate, err
}
func (r *paymentOrderRepo) CompletePaymentFulfillment(ctx context.Context, provider, tradeNo, token string, success bool, message string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) CompletePaymentFulfillment(ctx context.Context, provider, tradeNo, token string, success bool, message string) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.FulfillmentStatus != biz.FulfillmentStatusProcessing || po.FulfillmentToken != token {
return biz.ErrPaymentOrderBusy
if po.FulfillmentStatus != system.FulfillmentStatusProcessing || po.FulfillmentToken != token {
return system.ErrPaymentOrderBusy
}
po.FulfillmentToken = ""
po.FulfillmentLeaseUntil = nil
po.LastError = trimTo(message, 512)
if success {
po.FulfillmentStatus = biz.FulfillmentStatusSucceeded
po.FulfillmentStatus = system.FulfillmentStatusSucceeded
now := time.Now().UTC()
po.FulfilledAt = &now
} else {
po.FulfillmentStatus = biz.FulfillmentStatusFailed
po.FulfillmentStatus = system.FulfillmentStatusFailed
}
po.Version++
return tx.Save(po).Error
})
}
func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tradeNo string, amount int64, lease time.Duration) (*biz.PaymentOrder, string, error) {
func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tradeNo string, amount int64, lease time.Duration) (*system.PaymentOrder, string, error) {
var token string
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded {
return biz.ErrPaymentOrderState
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded {
return system.ErrPaymentOrderState
}
if amount <= 0 {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
now := time.Now().UTC()
if po.RefundStatus == biz.RefundStatusProcessing && po.RefundLeaseUntil != nil && po.RefundLeaseUntil.After(now) {
return biz.ErrPaymentOrderBusy
if po.RefundStatus == system.RefundStatusProcessing && po.RefundLeaseUntil != nil && po.RefundLeaseUntil.After(now) {
return system.ErrPaymentOrderBusy
}
if po.RefundStatus == biz.RefundStatusProcessing && po.RefundRequestedAmount != amount {
return biz.ErrPaymentOrderConflict
if po.RefundStatus == system.RefundStatusProcessing && po.RefundRequestedAmount != amount {
return system.ErrPaymentOrderConflict
}
if po.RefundStatus == biz.RefundStatusPending {
return biz.ErrPaymentOrderBusy
if po.RefundStatus == system.RefundStatusPending {
return system.ErrPaymentOrderBusy
}
// An expired processing lease means the provider outcome is unknown.
// Retry the same refund amount with the same durable refund number. A
// different amount must never reuse that operation identity.
reserved := int64(0)
if po.RefundStatus == biz.RefundStatusProcessing && po.RefundLeaseUntil != nil && !po.RefundLeaseUntil.After(now) {
if po.RefundStatus == system.RefundStatusProcessing && po.RefundLeaseUntil != nil && !po.RefundLeaseUntil.After(now) {
reserved = 0
} else {
reserved = po.RefundRequestedAmount
}
if amount > po.Amount-po.RefundedAmount-reserved {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
if lease <= 0 {
lease = 10 * time.Minute
@@ -370,7 +371,7 @@ func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tra
po.RefundNo = uuid.NewString()
}
until := now.Add(lease)
po.RefundStatus = biz.RefundStatusProcessing
po.RefundStatus = system.RefundStatusProcessing
po.RefundRequestedAmount = amount
po.RefundToken = token
po.RefundLeaseUntil = &until
@@ -381,18 +382,18 @@ func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tra
return order, token, err
}
func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, provider, tradeNo, token string, accepted bool, message string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, provider, tradeNo, token string, accepted bool, message string) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.RefundStatus != biz.RefundStatusProcessing || po.RefundToken != token {
return biz.ErrPaymentOrderBusy
if po.RefundStatus != system.RefundStatusProcessing || po.RefundToken != token {
return system.ErrPaymentOrderBusy
}
po.RefundToken = ""
po.RefundLeaseUntil = nil
po.LastError = trimTo(message, 512)
if accepted {
po.RefundStatus = biz.RefundStatusPending
po.RefundStatus = system.RefundStatusPending
} else {
po.RefundStatus = biz.RefundStatusFailed
po.RefundStatus = system.RefundStatusFailed
po.RefundRequestedAmount = 0
po.RefundNo = ""
}
@@ -401,26 +402,26 @@ func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, pro
})
}
func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.RefundStatus != biz.RefundStatusPending || po.RefundNo == "" || po.RefundNo != refundNo || po.RefundRequestedAmount != amount {
return biz.ErrPaymentOrderState
if po.RefundStatus != system.RefundStatusPending || po.RefundNo == "" || po.RefundNo != refundNo || po.RefundRequestedAmount != amount {
return system.ErrPaymentOrderState
}
po.LastError = trimTo(message, 512)
po.RefundRequestedAmount = 0
if !success {
po.RefundStatus = biz.RefundStatusFailed
po.RefundStatus = system.RefundStatusFailed
po.RefundNo = ""
po.Version++
return tx.Save(po).Error
}
po.RefundedAmount += amount
if po.RefundedAmount >= po.Amount {
po.PaymentStatus = biz.PaymentStatusRefunded
po.RefundStatus = biz.RefundStatusSucceeded
po.PaymentStatus = system.PaymentStatusRefunded
po.RefundStatus = system.RefundStatusSucceeded
} else {
po.PaymentStatus = biz.PaymentStatusPartiallyRefunded
po.RefundStatus = biz.RefundStatusPartial
po.PaymentStatus = system.PaymentStatusPartiallyRefunded
po.RefundStatus = system.RefundStatusPartial
}
po.RefundNo = ""
now := time.Now().UTC()
@@ -430,13 +431,13 @@ func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, t
})
}
func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*biz.PaymentOrder, error) {
var result *biz.PaymentOrder
func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*system.PaymentOrder, error) {
var result *system.PaymentOrder
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var po paymentOrderPO
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return biz.ErrPaymentOrderNotFound
return system.ErrPaymentOrderNotFound
}
return err
}
@@ -449,16 +450,16 @@ func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeN
return result, err
}
func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *biz.PaymentProviderUpdate) error {
func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *system.PaymentProviderUpdate) error {
if update.ProviderTradeNo != "" {
if po.ProviderTradeNo != nil && *po.ProviderTradeNo != update.ProviderTradeNo {
return biz.ErrPaymentProviderConflict
return system.ErrPaymentProviderConflict
}
var other paymentOrderPO
// This check is repeated under the order transaction so a platform
// transaction cannot be attached to a different merchant order.
if err := tx.Where("provider = ? AND provider_trade_no = ? AND id <> ?", po.Provider, update.ProviderTradeNo, po.ID).First(&other).Error; err == nil {
return biz.ErrPaymentProviderConflict
return system.ErrPaymentProviderConflict
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
@@ -467,7 +468,7 @@ func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *biz.PaymentP
}
if update.QueryID != "" {
if po.QueryID != "" && po.QueryID != update.QueryID {
return biz.ErrPaymentProviderConflict
return system.ErrPaymentProviderConflict
}
po.QueryID = update.QueryID
}
@@ -477,11 +478,11 @@ func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *biz.PaymentP
func normalizeOrderPaymentStatus(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "success", "paid", "fulfilled":
return biz.PaymentStatusPaid
return system.PaymentStatusPaid
case "pending", "created", "client_pending", "processing":
return biz.PaymentStatusPending
return system.PaymentStatusPending
case "failed", "closed", "cancelled", "canceled":
return biz.PaymentStatusFailed
return system.PaymentStatusFailed
default:
return ""
}
+26 -27
View File
@@ -2,10 +2,9 @@ package payment
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
)
func newPaymentOrderRepoForTest(t *testing.T) *paymentOrderRepo {
@@ -20,11 +19,11 @@ func newPaymentOrderRepoForTest(t *testing.T) *paymentOrderRepo {
return &paymentOrderRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
}
func testPaymentOrder() *biz.PaymentOrder {
return &biz.PaymentOrder{
TradeNo: "order-1", Provider: biz.PaymentAlipay, BusinessType: "game_item", BusinessID: "item-1",
Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: biz.PaymentStatusInitialized,
FulfillmentStatus: biz.FulfillmentStatusPending, RefundStatus: biz.RefundStatusNone,
func testPaymentOrder() *system.PaymentOrder {
return &system.PaymentOrder{
TradeNo: "order-1", Provider: system.PaymentAlipay, BusinessType: "game_item", BusinessID: "item-1",
Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: system.PaymentStatusInitialized,
FulfillmentStatus: system.FulfillmentStatusPending, RefundStatus: system.RefundStatusNone,
ConfirmationID: "11111111-1111-1111-1111-111111111111", RequestFingerprint: "fingerprint",
}
}
@@ -36,36 +35,36 @@ func TestPaymentOrderRepositoryPersistsPaymentFulfillmentAndRefundState(t *testi
if err != nil || !created {
t.Fatalf("create order = %#v created=%v err=%v", order, created, err)
}
update := &biz.PaymentProviderUpdate{Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY", EventID: "event-1"}
order, err = repo.ApplyPaymentResult(ctx, biz.PaymentAlipay, "order-1", update)
if err != nil || order.PaymentStatus != biz.PaymentStatusPaid || order.PaidAmount != 100 {
update := &system.PaymentProviderUpdate{Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY", EventID: "event-1"}
order, err = repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-1", update)
if err != nil || order.PaymentStatus != system.PaymentStatusPaid || order.PaidAmount != 100 {
t.Fatalf("apply payment = %#v err=%v", order, err)
}
order, token, duplicate, err := repo.BeginPaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", time.Minute)
if err != nil || duplicate || token == "" || order.FulfillmentStatus != biz.FulfillmentStatusProcessing {
order, token, duplicate, err := repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute)
if err != nil || duplicate || token == "" || order.FulfillmentStatus != system.FulfillmentStatusProcessing {
t.Fatalf("begin fulfillment = %#v token=%q duplicate=%v err=%v", order, token, duplicate, err)
}
if _, _, _, err = repo.BeginPaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", time.Minute); err == nil {
if _, _, _, err = repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute); err == nil {
t.Fatal("concurrent fulfillment was accepted")
}
order, err = repo.CompletePaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.FulfillmentStatus != biz.FulfillmentStatusSucceeded {
order, err = repo.CompletePaymentFulfillment(ctx, system.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.FulfillmentStatus != system.FulfillmentStatusSucceeded {
t.Fatalf("complete fulfillment = %#v err=%v", order, err)
}
_, _, duplicate, err = repo.BeginPaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", time.Minute)
_, _, duplicate, err = repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute)
if err != nil || !duplicate {
t.Fatalf("duplicate fulfillment = duplicate=%v err=%v", duplicate, err)
}
order, token, err = repo.BeginPaymentRefund(ctx, biz.PaymentAlipay, "order-1", 40, time.Minute)
if err != nil || token == "" || order.RefundStatus != biz.RefundStatusProcessing {
order, token, err = repo.BeginPaymentRefund(ctx, system.PaymentAlipay, "order-1", 40, time.Minute)
if err != nil || token == "" || order.RefundStatus != system.RefundStatusProcessing {
t.Fatalf("begin refund = %#v token=%q err=%v", order, token, err)
}
order, err = repo.CompletePaymentRefundRequest(ctx, biz.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.RefundStatus != biz.RefundStatusPending || order.RefundRequestedAmount != 40 {
order, err = repo.CompletePaymentRefundRequest(ctx, system.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.RefundStatus != system.RefundStatusPending || order.RefundRequestedAmount != 40 {
t.Fatalf("accept refund = %#v err=%v", order, err)
}
order, err = repo.ConfirmPaymentRefund(ctx, biz.PaymentAlipay, "order-1", order.RefundNo, 40, true, "")
if err != nil || order.RefundedAmount != 40 || order.PaymentStatus != biz.PaymentStatusPartiallyRefunded {
order, err = repo.ConfirmPaymentRefund(ctx, system.PaymentAlipay, "order-1", order.RefundNo, 40, true, "")
if err != nil || order.RefundedAmount != 40 || order.PaymentStatus != system.PaymentStatusPartiallyRefunded {
t.Fatalf("confirm refund = %#v err=%v", order, err)
}
}
@@ -83,11 +82,11 @@ func TestPaymentOrderRepositoryRejectsProviderTradeReuse(t *testing.T) {
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
t.Fatal(err)
}
update := &biz.PaymentProviderUpdate{Status: "success", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY"}
if _, err := repo.ApplyPaymentResult(ctx, biz.PaymentAlipay, "order-1", update); err != nil {
update := &system.PaymentProviderUpdate{Status: "success", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY"}
if _, err := repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-1", update); err != nil {
t.Fatal(err)
}
if _, err := repo.ApplyPaymentResult(ctx, biz.PaymentAlipay, "order-2", update); err != biz.ErrPaymentProviderConflict {
if _, err := repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-2", update); err != system.ErrPaymentProviderConflict {
t.Fatalf("provider trade reuse err = %v", err)
}
}
@@ -101,13 +100,13 @@ func TestPaymentOrderRepositoryListsWithFilters(t *testing.T) {
}
second := testPaymentOrder()
second.TradeNo = "wechat-order-2"
second.Provider = biz.PaymentWechatV3
second.Provider = system.PaymentWechatV3
second.BusinessID = "item-2"
second.ConfirmationID = "33333333-3333-3333-3333-333333333333"
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
t.Fatal(err)
}
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, biz.PaymentOrderFilter{Provider: biz.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, system.PaymentOrderFilter{Provider: system.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
if err != nil {
t.Fatal(err)
}
+13 -13
View File
@@ -3,9 +3,9 @@ package system
import (
"context"
"encoding/json"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/gormkit"
"kra/pkg/database/pagination"
@@ -27,17 +27,17 @@ func (announcementPO) TableName() string { return "sys_announcements" }
type announcementRepo struct{ data Provider }
func NewAnnouncementRepo(data Provider) biz.AnnouncementRepo { return &announcementRepo{data: data} }
func NewAnnouncementRepo(data Provider) system.AnnouncementRepo { return &announcementRepo{data: data} }
func newAnnouncement(item *biz.Announcement) announcementPO {
func newAnnouncement(item *system.Announcement) announcementPO {
return announcementPO{ID: item.ID, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: gormkit.JSON(item.Attachments)}
}
func announcementToBiz(item announcementPO) *biz.Announcement {
return &biz.Announcement{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: json.RawMessage(item.Attachments)}
func announcementToBiz(item announcementPO) *system.Announcement {
return &system.Announcement{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: json.RawMessage(item.Attachments)}
}
func (r *announcementRepo) Create(ctx context.Context, item *biz.Announcement) error {
func (r *announcementRepo) Create(ctx context.Context, item *system.Announcement) error {
po := newAnnouncement(item)
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@@ -54,12 +54,12 @@ func (r *announcementRepo) DeleteByIDs(ctx context.Context, ids []string) error
return r.data.DB().WithContext(ctx).Delete(&[]announcementPO{}, "id IN ?", ids).Error
}
func (r *announcementRepo) Update(ctx context.Context, item *biz.Announcement) error {
func (r *announcementRepo) Update(ctx context.Context, item *system.Announcement) error {
po := newAnnouncement(item)
return r.data.DB().WithContext(ctx).Model(&announcementPO{}).Where("id = ?", item.ID).Updates(&po).Error
}
func (r *announcementRepo) Find(ctx context.Context, id string) (*biz.Announcement, error) {
func (r *announcementRepo) Find(ctx context.Context, id string) (*system.Announcement, error) {
var po announcementPO
if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return nil, err
@@ -67,7 +67,7 @@ func (r *announcementRepo) Find(ctx context.Context, id string) (*biz.Announceme
return announcementToBiz(po), nil
}
func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilter) ([]*biz.Announcement, int64, error) {
func (r *announcementRepo) List(ctx context.Context, filter system.AnnouncementFilter) ([]*system.Announcement, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&announcementPO{})
if filter.StartCreatedAt != nil && filter.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", filter.StartCreatedAt, filter.EndCreatedAt)
@@ -83,14 +83,14 @@ func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilt
if err := db.Find(&pos).Error; err != nil {
return nil, 0, err
}
items := make([]*biz.Announcement, 0, len(pos))
items := make([]*system.Announcement, 0, len(pos))
for _, po := range pos {
items = append(items, announcementToBiz(po))
}
return items, total, nil
}
func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, error) {
func (r *announcementRepo) UserOptions(ctx context.Context) ([]system.UserOption, error) {
var rows []struct {
Label string
Value uint
@@ -98,9 +98,9 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
// 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
items := make([]biz.UserOption, 0, len(rows))
items := make([]system.UserOption, 0, len(rows))
for _, row := range rows {
items = append(items, biz.UserOption{Label: row.Label, Value: row.Value})
items = append(items, system.UserOption{Label: row.Label, Value: row.Value})
}
return items, nil
}
@@ -2,16 +2,15 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
)
func TestAnnouncementRepositoryKeepsRawIDQuerySemantics(t *testing.T) {
data := newTransactionTestData(t)
repo := &announcementRepo{data: data}
ctx := context.Background()
item := &biz.Announcement{Title: "notice"}
item := &system.Announcement{Title: "notice"}
if err := repo.Create(ctx, item); err != nil {
t.Fatal(err)
}
@@ -44,7 +43,7 @@ func TestAnnouncementRepositoryPreservesSignedUserID(t *testing.T) {
repo := &announcementRepo{data: data}
ctx := context.Background()
userID := -1
item := &biz.Announcement{Title: "notice", UserID: &userID}
item := &system.Announcement{Title: "notice", UserID: &userID}
if err := repo.Create(ctx, item); err != nil {
t.Fatal(err)
}
+10 -10
View File
@@ -3,10 +3,10 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"strconv"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -14,7 +14,7 @@ import (
type apiRepo struct{ data Provider }
func NewAPIRepo(data Provider) biz.APIRepo { return &apiRepo{data: data} }
func NewAPIRepo(data Provider) system.APIRepo { return &apiRepo{data: data} }
type apiPO struct {
ID uint `gorm:"primaryKey"`
@@ -47,10 +47,10 @@ type authorityAPIPO struct {
func (authorityAPIPO) TableName() string { return "sys_authority_apis" }
func apiFromPO(po apiPO) *biz.API {
return &biz.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method}
func apiFromPO(po apiPO) *system.API {
return &system.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method}
}
func (r *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error {
func (r *apiRepo) CreateAPI(ctx context.Context, v *system.API) error {
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: v.Method}
var count int64
if err := r.data.DB().WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil {
@@ -65,7 +65,7 @@ func (r *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error {
v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method
return nil
}
func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
func (r *apiRepo) UpdateAPI(ctx context.Context, v *system.API) error {
db := r.data.DB().WithContext(ctx)
var old apiPO
if err := db.First(&old, v.ID).Error; err != nil {
@@ -109,18 +109,18 @@ func (r *apiRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
return tx.Delete(&apiPO{}, ids).Error
})
}
func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*system.API, error) {
var po apiPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return apiFromPO(po), nil
}
func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) {
func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *system.API) ([]*system.API, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&apiPO{})
if q != nil && q.StrictAll {
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
if actor, ok := system.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, 0, err
@@ -173,7 +173,7 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
if err := query.Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.API, 0, len(pos))
out := make([]*system.API, 0, len(pos))
for _, po := range pos {
out = append(out, apiFromPO(po))
}
+8 -9
View File
@@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"strconv"
"kra/internal/biz"
"github.com/casbin/casbin/v3"
casbinmodel "github.com/casbin/casbin/v3/model"
"gorm.io/gorm"
@@ -43,7 +42,7 @@ func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []ui
return errors.New("您提交的角色ID不合法")
}
}
if err := r.checkPolicyPathsAuth(ctx, []*biz.API{{Path: path, Method: method}}); err != nil {
if err := r.checkPolicyPathsAuth(ctx, []*system.API{{Path: path, Method: method}}); err != nil {
return err
}
}
@@ -105,21 +104,21 @@ m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act`)
}
return enforcer.Enforce(subject, path, method)
}
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*system.API, error) {
rows, err := policyRowsForAuthority(r.data.DB().WithContext(ctx), aid)
if err != nil {
return nil, err
}
var out []*biz.API
var out []*system.API
if len(rows) > 0 {
out = make([]*biz.API, 0, len(rows))
out = make([]*system.API, 0, len(rows))
}
for _, row := range rows {
out = append(out, &biz.API{Path: row.V1, Method: row.V2})
out = append(out, &system.API{Path: row.V1, Method: row.V2})
}
return out, nil
}
func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*system.API) error {
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil {
return err
}
@@ -155,7 +154,7 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
return nil
}
func (r *apiRepo) checkPolicyPathsAuth(ctx context.Context, paths []*biz.API) error {
func (r *apiRepo) checkPolicyPathsAuth(ctx context.Context, paths []*system.API) error {
actor, _, strict, err := (&authorityAccessRepo{data: r.data}).strictAuthorityAccess(ctx)
if err != nil || !strict {
return err
+10 -10
View File
@@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@@ -33,7 +33,7 @@ func apiPolicyUintPointer(value uint) *uint { return &value }
func TestSetPolicyPathsStrictRootRequiresRegisteredAPI(t *testing.T) {
data := newPolicyTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 888})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 888})
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{
{AuthorityID: 888, ParentID: apiPolicyUintPointer(0)},
{AuthorityID: 999, ParentID: apiPolicyUintPointer(888)},
@@ -44,18 +44,18 @@ func TestSetPolicyPathsStrictRootRequiresRegisteredAPI(t *testing.T) {
t.Fatal(err)
}
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*biz.API{{Path: "/unknown", Method: "POST"}})
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*system.API{{Path: "/unknown", Method: "POST"}})
if err == nil || !strings.Contains(err.Error(), "存在api不在权限列表中") {
t.Fatalf("unregistered root assignment error = %v", err)
}
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*biz.API{{Path: "/known", Method: "POST"}}); err != nil {
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*system.API{{Path: "/known", Method: "POST"}}); err != nil {
t.Fatal(err)
}
}
func TestSetPolicyPathsStrictChildRejectsStalePolicy(t *testing.T) {
data := newPolicyTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 1001})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 1001})
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{
{AuthorityID: 888, ParentID: apiPolicyUintPointer(0)},
{AuthorityID: 1001, ParentID: apiPolicyUintPointer(888)},
@@ -73,18 +73,18 @@ func TestSetPolicyPathsStrictChildRejectsStalePolicy(t *testing.T) {
t.Fatal(err)
}
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*biz.API{{Path: "/stale", Method: "POST"}})
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*system.API{{Path: "/stale", Method: "POST"}})
if err == nil || !strings.Contains(err.Error(), "存在api不在权限列表中") {
t.Fatalf("stale child assignment error = %v", err)
}
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*biz.API{{Path: "/known", Method: "POST"}}); err != nil {
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*system.API{{Path: "/known", Method: "POST"}}); err != nil {
t.Fatal(err)
}
}
func TestSetPolicyPathsUsesCompatibleDedupeKey(t *testing.T) {
data := newPolicyTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 888})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 888})
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{
{AuthorityID: 888, ParentID: apiPolicyUintPointer(0)},
{AuthorityID: 999, ParentID: apiPolicyUintPointer(888)},
@@ -98,7 +98,7 @@ func TestSetPolicyPathsUsesCompatibleDedupeKey(t *testing.T) {
t.Fatal(err)
}
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*biz.API{
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*system.API{
{Path: "/a", Method: "BC"},
{Path: "/aB", Method: "C"},
}); err != nil {
@@ -148,7 +148,7 @@ func TestSetAPIRolesStrictOnlyChangesManagedAuthorities(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &apiRepo{data: data}
if err := repo.SetAPIRoles(ctx, "/known", "POST", []uint{siblingID}); err == nil {
t.Fatal("SetAPIRoles() accepted an out-of-scope authority")
+5 -6
View File
@@ -2,20 +2,19 @@ package system
import (
"context"
"kra/internal/biz"
"kra/internal/biz/system"
"gorm.io/gorm"
)
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*system.API, error) {
var pos []ignoredAPIPO
if err := r.data.DB().WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.API, 0, len(pos))
out := make([]*system.API, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.API{Path: po.Path, Method: po.Method})
out = append(out, &system.API{Path: po.Path, Method: po.Method})
}
return out, nil
}
@@ -28,7 +27,7 @@ func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignore
}
return r.data.DB().WithContext(ctx).Unscoped().Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
}
func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*system.API) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if len(added) > 0 {
pos := make([]apiPO, 0, len(added))
+8 -8
View File
@@ -3,9 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -13,7 +13,7 @@ import (
type apiTokenRepo struct{ data Provider }
func NewAPITokenRepo(data Provider) biz.APITokenRepo { return &apiTokenRepo{data: data} }
func NewAPITokenRepo(data Provider) system.APITokenRepo { return &apiTokenRepo{data: data} }
type apiTokenPO struct {
ID uint `gorm:"primaryKey"`
@@ -40,7 +40,7 @@ type jwtBlacklistPO struct {
func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" }
func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) {
func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*system.User, bool, error) {
var po userPO
if err := r.data.DB().WithContext(ctx).First(&po, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -59,7 +59,7 @@ func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID
user, err := (&userRepo{data: r.data}).loadUser(ctx, &po)
return user, count > 0 || po.AuthorityID == authorityID, err
}
func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error {
func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *system.APIToken) error {
po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@@ -68,7 +68,7 @@ func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) erro
v.CreatedAt = po.CreatedAt
return nil
}
func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) {
func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*system.APIToken, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&apiTokenPO{})
if userID != 0 {
db = db.Where("user_id = ?", userID)
@@ -94,13 +94,13 @@ func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID
return nil, 0, err
}
}
users := make(map[uint]*biz.User, len(userPOs))
users := make(map[uint]*system.User, len(userPOs))
for i := range userPOs {
users[userPOs[i].ID] = baseBizUser(&userPOs[i])
}
out := make([]*biz.APIToken, 0, len(pos))
out := make([]*system.APIToken, 0, len(pos))
for _, po := range pos {
v := &biz.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark, User: users[po.UserID]}
v := &system.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark, User: users[po.UserID]}
out = append(out, v)
}
return out, total, nil
+8 -4
View File
@@ -1,13 +1,17 @@
package system
import "kra/internal/biz"
import (
"kra/internal/biz/system"
)
type auditQueryRepo struct{ data Provider }
type auditRecorderRepo struct{ data Provider }
type logFileRepo struct{ data Provider }
func NewAuditRepo(data Provider) biz.AuditQueryRepo { return &auditQueryRepo{data: data} }
func NewAuditRepo(data Provider) system.AuditQueryRepo { return &auditQueryRepo{data: data} }
func NewAuditRecorderRepo(data Provider) biz.AuditRecordRepo { return &auditRecorderRepo{data: data} }
func NewAuditRecorderRepo(data Provider) system.AuditRecordRepo {
return &auditRecorderRepo{data: data}
}
func NewLogFileRepo(data Provider) biz.LogFileRepo { return &logFileRepo{data: data} }
func NewLogFileRepo(data Provider) system.LogFileRepo { return &logFileRepo{data: data} }
+18 -19
View File
@@ -3,8 +3,7 @@ package system
import (
"context"
"errors"
"kra/internal/biz"
"kra/internal/biz/system"
"gorm.io/gorm"
)
@@ -13,7 +12,7 @@ var errInvalidDataScope = errors.New("数据权限范围不合法")
type authorityAccessRepo struct{ data Provider }
func NewAuthorityAccessRepo(data Provider) biz.AuthorityAccessRepo {
func NewAuthorityAccessRepo(data Provider) system.AuthorityAccessRepo {
return &authorityAccessRepo{data: data}
}
@@ -58,18 +57,18 @@ func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID ui
return allowed, nil
}
func (r *authorityAccessRepo) strictAuthorityAccess(ctx context.Context) (biz.Actor, map[uint]bool, bool, error) {
func (r *authorityAccessRepo) strictAuthorityAccess(ctx context.Context) (system.Actor, map[uint]bool, bool, error) {
config := r.data.Runtime().Admin()
if config == nil || config.System == nil || !config.System.UseStrictAuth {
return biz.Actor{}, nil, false, nil
return system.Actor{}, nil, false, nil
}
actor, ok := biz.ActorFromContext(ctx)
actor, ok := system.ActorFromContext(ctx)
if !ok {
return biz.Actor{}, nil, true, errors.New("您提交的角色ID不合法")
return system.Actor{}, nil, true, errors.New("您提交的角色ID不合法")
}
allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
if err != nil {
return biz.Actor{}, nil, true, err
return system.Actor{}, nil, true, err
}
return actor, allowed, true, nil
}
@@ -91,7 +90,7 @@ func (r *authorityAccessRepo) checkAuthorityIDAuth(ctx context.Context, targetID
return r.checkAuthorityIDsAuth(ctx, []uint{targetID})
}
func managedAuthorityParent(actor biz.Actor, allowed map[uint]bool, targetID uint, parentID *uint, creating bool) (*uint, error) {
func managedAuthorityParent(actor system.Actor, allowed map[uint]bool, targetID uint, parentID *uint, creating bool) (*uint, error) {
if creating && (parentID == nil || *parentID == 0) {
value := actor.AuthorityID
return &value, nil
@@ -131,7 +130,7 @@ func (r *authorityAccessRepo) ensureAuthorityParentAcyclic(ctx context.Context,
return nil
}
func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *system.Authority) error {
if value.DataScope == 0 {
value.DataScope = 1
} else if value.DataScope < 1 || value.DataScope > 5 {
@@ -168,7 +167,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
}
value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt
value.DataScope, value.DefaultRouter = po.DataScope, po.DefaultRouter
value.Menus = []*biz.Menu{{ID: 1, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Title: "仪表盘", Icon: "setting"}}
value.Menus = []*system.Menu{{ID: 1, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Title: "仪表盘", Icon: "setting"}}
var dashboard menuPO
if err := tx.Where("name = ?", "dashboard").First(&dashboard).Error; err == nil {
grantDashboard := true
@@ -209,7 +208,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
return nil
})
}
func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error {
func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *system.Authority) error {
if value.DataScope == 0 {
value.DataScope = 1
} else if value.DataScope < 1 || value.DataScope > 5 {
@@ -283,7 +282,7 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
if err := tx.Where("id IN ?", menuIDs).Order("sort").Find(&copiedMenus).Error; err != nil {
return err
}
value.Menus = make([]*biz.Menu, 0, len(copiedMenus))
value.Menus = make([]*system.Menu, 0, len(copiedMenus))
for _, menu := range copiedMenus {
value.Menus = append(value.Menus, menuFromPO(menu))
}
@@ -362,7 +361,7 @@ func (r *authorityAccessRepo) copyPolicyAllowed(ctx context.Context, tx *gorm.DB
}
return policyExists(tx.WithContext(ctx), actorID, path, method)
}
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *system.Authority) error {
if err := r.checkAuthorityIDAuth(ctx, value.AuthorityID); err != nil {
return err
}
@@ -464,11 +463,11 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
return tx.Unscoped().Delete(&authorityPO{}, "authority_id = ?", id).Error
})
}
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*system.Authority, error) {
db := r.data.DB().WithContext(ctx)
var allowed map[uint]bool
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok {
if actor, ok := system.ActorFromContext(ctx); ok {
// The current authority is loaded even when strict mode is disabled;
// an invalid token authority therefore fails the list request instead of
// exposing the complete role tree.
@@ -488,7 +487,7 @@ func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Autho
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Authority, 0, len(pos))
out := make([]*system.Authority, 0, len(pos))
for _, po := range pos {
if allowed != nil && !allowed[po.AuthorityID] {
continue
@@ -752,8 +751,8 @@ func (r *authorityAccessRepo) DataScopeDepartmentIDs(ctx context.Context, id uin
err := r.data.DB().WithContext(ctx).Model(&authorityDepartmentPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_department_id", &ids).Error
return ids, err
}
func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (system.DataScope, error) {
identity := system.DataScope{UserID: userID, AuthorityID: authorityID}
var user userPO
if err := r.data.DB().WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error; err != nil {
return identity, err
+31 -31
View File
@@ -3,9 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"kra/internal/conf"
"gorm.io/gorm"
@@ -44,8 +44,8 @@ func TestCopyAuthorityStrictPolicyValidationMatchesAdministrationContract(t *tes
}
repo := &authorityAccessRepo{data: data}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: rootID})
created := &biz.Authority{AuthorityID: 910, AuthorityName: "copy", ParentID: &rootID}
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: rootID})
created := &system.Authority{AuthorityID: 910, AuthorityName: "copy", ParentID: &rootID}
if err := repo.CopyAuthority(ctx, 900, created); err != nil {
t.Fatalf("root copy of a registered API failed: %v", err)
}
@@ -53,7 +53,7 @@ func TestCopyAuthorityStrictPolicyValidationMatchesAdministrationContract(t *tes
t.Fatalf("copied policy exists = %v, err = %v", exists, err)
}
staleCopy := &biz.Authority{AuthorityID: 911, AuthorityName: "stale-copy", ParentID: &rootID}
staleCopy := &system.Authority{AuthorityID: 911, AuthorityName: "stale-copy", ParentID: &rootID}
if err := repo.CopyAuthority(ctx, 901, staleCopy); err == nil || err.Error() != "存在api不在权限列表中" {
t.Fatalf("stale API copy error = %v", err)
}
@@ -78,8 +78,8 @@ func TestCopyAuthorityDuplicateIDWinsOverStrictParentValidation(t *testing.T) {
t.Fatal(err)
}
invalidParent := uint(999999)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: rootID})
err := (&authorityAccessRepo{data: data}).CopyAuthority(ctx, 0, &biz.Authority{AuthorityID: 920, AuthorityName: "duplicate", ParentID: &invalidParent})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: rootID})
err := (&authorityAccessRepo{data: data}).CopyAuthority(ctx, 0, &system.Authority{AuthorityID: 920, AuthorityName: "duplicate", ParentID: &invalidParent})
if err == nil || err.Error() != "存在相同角色id" {
t.Fatalf("duplicate copy error = %v", err)
}
@@ -87,7 +87,7 @@ func TestCopyAuthorityDuplicateIDWinsOverStrictParentValidation(t *testing.T) {
func TestListAuthoritiesRequiresCurrentAuthorityOutsideStrictMode(t *testing.T) {
data := newTransactionTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 999999})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 999999})
if _, err := (&authorityAccessRepo{data: data}).ListAuthorities(ctx); !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("missing current authority error = %v", err)
}
@@ -198,7 +198,7 @@ func TestSetAuthorityUsersStrictRejectsUsersOutsideManagedRoles(t *testing.T) {
if err := db.Create(&users).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.SetAuthorityUsers(ctx, childID, []uint{users[1].ID}); err == nil {
t.Fatal("SetAuthorityUsers() accepted a user outside the managed role tree")
@@ -231,7 +231,7 @@ func TestSetAuthorityUsersStrictRejectsMixedRoleUserAlreadyLinked(t *testing.T)
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
if err := (&authorityAccessRepo{data: data}).SetAuthorityUsers(ctx, childID, nil); err == nil {
t.Fatal("SetAuthorityUsers() modified a linked user that also has an out-of-scope role")
}
@@ -257,8 +257,8 @@ func TestUpdateAuthorityStrictRejectsHierarchyCycle(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
err := (&authorityAccessRepo{data: data}).UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &childID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
err := (&authorityAccessRepo{data: data}).UpdateAuthority(ctx, &system.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &childID})
if err == nil {
t.Fatal("UpdateAuthority() accepted a parent that forms a cycle")
}
@@ -283,7 +283,7 @@ func TestSetDataScopeValidatesScopeAndStrictDepartments(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.SetDataScope(ctx, childID, 0, nil); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("SetDataScope() invalid scope error = %v", err)
@@ -323,7 +323,7 @@ func TestSetDataScopeStrictRejectsDepartmentsOutsideActorScope(t *testing.T) {
if err := db.Create(&authorityDepartmentPO{AuthorityID: actorID, DepartmentID: departments[0].ID}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: actorUser.ID, AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.SetDataScope(ctx, childID, 5, []uint{departments[1].ID}); err == nil {
t.Fatal("SetDataScope() accepted a department outside the actor's data scope")
@@ -350,31 +350,31 @@ func TestStrictDataScopeGrantRejectsBroaderChildScopes(t *testing.T) {
if err := db.Create(&actorUser).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: actorUser.ID, AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
created := &biz.Authority{AuthorityID: 1373, AuthorityName: "created", ParentID: &actorID}
created := &system.Authority{AuthorityID: 1373, AuthorityName: "created", ParentID: &actorID}
if err := repo.CreateAuthority(ctx, created); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CreateAuthority() broader scope error = %v", err)
}
if created.DataScope != 1 {
t.Fatalf("CreateAuthority() did not normalize zero scope before validation: %d", created.DataScope)
}
copied := &biz.Authority{AuthorityID: 1374, AuthorityName: "copied", ParentID: &actorID}
copied := &system.Authority{AuthorityID: 1374, AuthorityName: "copied", ParentID: &actorID}
if err := repo.CopyAuthority(ctx, sourceID, copied); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CopyAuthority() broader scope error = %v", err)
}
if copied.DataScope != 1 {
t.Fatalf("CopyAuthority() did not normalize zero scope before validation: %d", copied.DataScope)
}
if err := repo.UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 2}); !errors.Is(err, errInvalidDataScope) {
if err := repo.UpdateAuthority(ctx, &system.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 2}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("UpdateAuthority() broader scope error = %v", err)
}
if err := repo.SetDataScope(ctx, targetID, 1, nil); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("SetDataScope() broader scope error = %v", err)
}
allowed := &biz.Authority{AuthorityID: 1375, AuthorityName: "self-only", ParentID: &actorID, DataScope: 4}
allowed := &system.Authority{AuthorityID: 1375, AuthorityName: "self-only", ParentID: &actorID, DataScope: 4}
if err := repo.CreateAuthority(ctx, allowed); err != nil {
t.Fatalf("CreateAuthority() rejected a narrower scope: %v", err)
}
@@ -405,9 +405,9 @@ func TestCreateAuthorityStrictDefaultsStayWithinActorPermissions(t *testing.T) {
if err := db.Create(&apiPO{Path: "/menu/getMenu", Method: "POST"}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
first := &biz.Authority{AuthorityID: 1401, AuthorityName: "no-defaults", ParentID: &actorID}
first := &system.Authority{AuthorityID: 1401, AuthorityName: "no-defaults", ParentID: &actorID}
if err := repo.CreateAuthority(ctx, first); err != nil {
t.Fatal(err)
}
@@ -429,7 +429,7 @@ func TestCreateAuthorityStrictDefaultsStayWithinActorPermissions(t *testing.T) {
if err := db.Create(&policy).Error; err != nil {
t.Fatal(err)
}
second := &biz.Authority{AuthorityID: 1402, AuthorityName: "owned-defaults", ParentID: &actorID}
second := &system.Authority{AuthorityID: 1402, AuthorityName: "owned-defaults", ParentID: &actorID}
if err := repo.CreateAuthority(ctx, second); err != nil {
t.Fatal(err)
}
@@ -476,21 +476,21 @@ func TestCopyAuthorityStrictValidatesCopiedMenusAndButtons(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1510, AuthorityName: "menu-fail", ParentID: &actorID}); err == nil {
if err := repo.CopyAuthority(ctx, sourceID, &system.Authority{AuthorityID: 1510, AuthorityName: "menu-fail", ParentID: &actorID}); err == nil {
t.Fatal("CopyAuthority() copied a menu not assigned to the actor")
}
if err := db.Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", sourceID, 11).Delete(&authorityMenuPO{}).Error; err != nil {
t.Fatal(err)
}
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1511, AuthorityName: "button-fail", ParentID: &actorID}); err == nil {
if err := repo.CopyAuthority(ctx, sourceID, &system.Authority{AuthorityID: 1511, AuthorityName: "button-fail", ParentID: &actorID}); err == nil {
t.Fatal("CopyAuthority() copied a button not assigned to the actor")
}
if err := db.Where("authority_id = ? AND sys_base_menu_btn_id = ?", sourceID, 32).Delete(&authorityButtonPO{}).Error; err != nil {
t.Fatal(err)
}
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1512, AuthorityName: "valid", ParentID: &actorID}); err != nil {
if err := repo.CopyAuthority(ctx, sourceID, &system.Authority{AuthorityID: 1512, AuthorityName: "valid", ParentID: &actorID}); err != nil {
t.Fatalf("CopyAuthority() rejected owned permissions: %v", err)
}
for _, failedID := range []uint{1510, 1511} {
@@ -509,7 +509,7 @@ func TestAuthorityCreateAndCopyNormalizeZeroDataScope(t *testing.T) {
db := data.gormDB.WithContext(context.Background())
repo := &authorityAccessRepo{data: data}
created := &biz.Authority{AuthorityID: 1600, AuthorityName: "created"}
created := &system.Authority{AuthorityID: 1600, AuthorityName: "created"}
if err := repo.CreateAuthority(context.Background(), created); err != nil {
t.Fatal(err)
}
@@ -520,7 +520,7 @@ func TestAuthorityCreateAndCopyNormalizeZeroDataScope(t *testing.T) {
if err := db.Create(&authorityPO{AuthorityID: 1601, AuthorityName: "source", DataScope: 3}).Error; err != nil {
t.Fatal(err)
}
copied := &biz.Authority{AuthorityID: 1602, AuthorityName: "copied"}
copied := &system.Authority{AuthorityID: 1602, AuthorityName: "copied"}
if err := repo.CopyAuthority(context.Background(), 1601, copied); err != nil {
t.Fatal(err)
}
@@ -549,13 +549,13 @@ func TestAuthorityMutationsRejectInvalidDataScope(t *testing.T) {
}
for _, scope := range []int{-1, 6} {
if err := repo.CreateAuthority(context.Background(), &biz.Authority{AuthorityID: uint(1800 + scope + 1), AuthorityName: "invalid-create", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
if err := repo.CreateAuthority(context.Background(), &system.Authority{AuthorityID: uint(1800 + scope + 1), AuthorityName: "invalid-create", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CreateAuthority() scope %d error = %v", scope, err)
}
if err := repo.CopyAuthority(context.Background(), 1700, &biz.Authority{AuthorityID: uint(1900 + scope + 1), AuthorityName: "invalid-copy", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
if err := repo.CopyAuthority(context.Background(), 1700, &system.Authority{AuthorityID: uint(1900 + scope + 1), AuthorityName: "invalid-copy", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CopyAuthority() scope %d error = %v", scope, err)
}
if err := repo.UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 1701, AuthorityName: "invalid-update", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
if err := repo.UpdateAuthority(context.Background(), &system.Authority{AuthorityID: 1701, AuthorityName: "invalid-update", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("UpdateAuthority() scope %d error = %v", scope, err)
}
}
@@ -568,7 +568,7 @@ func TestUpdateAuthorityZeroDataScopeKeepsStoredValue(t *testing.T) {
t.Fatal(err)
}
if err := (&authorityAccessRepo{data: data}).UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 2000, AuthorityName: "after", DataScope: 0}); err != nil {
if err := (&authorityAccessRepo{data: data}).UpdateAuthority(context.Background(), &system.Authority{AuthorityID: 2000, AuthorityName: "after", DataScope: 0}); err != nil {
t.Fatal(err)
}
var stored authorityPO
+6 -6
View File
@@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -23,13 +23,13 @@ type dataAccessLogPO struct {
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
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) *biz.DataAccessLog {
return &biz.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 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 *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
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 {
if q.EventType != "" {
@@ -47,7 +47,7 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DataAccessLog, 0, len(pos))
out := make([]*system.DataAccessLog, 0, len(pos))
for _, po := range pos {
out = append(out, dataAccessFromPO(po))
}
+15 -16
View File
@@ -3,18 +3,17 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"strconv"
"strings"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type departmentRepo struct{ data Provider }
func NewDepartmentRepo(data Provider) biz.DepartmentRepo { return &departmentRepo{data: data} }
func NewDepartmentRepo(data Provider) system.DepartmentRepo { return &departmentRepo{data: data} }
type departmentPO struct {
ID uint `gorm:"primaryKey"`
@@ -47,11 +46,11 @@ type authorityDepartmentPO struct {
func (authorityDepartmentPO) TableName() string { return "sys_authority_departments" }
func deptFromPO(po departmentPO) *biz.Department {
return &biz.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status}
func deptFromPO(po departmentPO) *system.Department {
return &system.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status}
}
func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, departments []*biz.Department) error {
func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, departments []*system.Department) error {
leaderIDs := make([]uint, 0, len(departments))
for _, department := range departments {
if department.LeaderID != 0 {
@@ -65,7 +64,7 @@ func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, department
if err := r.data.DB().WithContext(ctx).Where("id IN ?", leaderIDs).Find(&leaders).Error; err != nil {
return err
}
leaderByID := make(map[uint]*biz.User, len(leaders))
leaderByID := make(map[uint]*system.User, len(leaders))
for i := range leaders {
leaderByID[leaders[i].ID] = baseBizUser(&leaders[i])
}
@@ -74,7 +73,7 @@ func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, department
}
return nil
}
func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department) error {
func (r *departmentRepo) CreateDepartment(ctx context.Context, v *system.Department) error {
v.Ancestors = "0"
if v.ParentID != 0 {
var parent departmentPO
@@ -88,7 +87,7 @@ func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department
}
return r.data.DB().WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error
}
func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error {
func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *system.Department) error {
if v.ParentID == v.ID {
return errors.New("父部门不能是自己")
}
@@ -130,18 +129,18 @@ func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
}
return r.data.DB().WithContext(ctx).Delete(&departmentPO{}, id).Error
}
func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) {
func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*system.Department, error) {
var po departmentPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
department := deptFromPO(po)
if err := r.attachDepartmentLeaders(ctx, []*biz.Department{department}); err != nil {
if err := r.attachDepartmentLeaders(ctx, []*system.Department{department}); err != nil {
return nil, err
}
return department, nil
}
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*system.Department, error) {
var pos []departmentPO
db := r.data.DB().WithContext(ctx).Order("sort")
if name != "" {
@@ -150,15 +149,15 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
nodes := map[uint]*biz.Department{}
items := make([]*biz.Department, 0, len(pos))
nodes := map[uint]*system.Department{}
items := make([]*system.Department, 0, len(pos))
for _, po := range pos {
item := deptFromPO(po)
if name == "" {
// Recursive GORM child queries initialize leaf collections to an empty
// slice. Preserve that tree response shape while search and single-item
// queries keep the model's nil children value.
item.Children = []*biz.Department{}
item.Children = []*system.Department{}
}
nodes[po.ID] = item
items = append(items, item)
@@ -169,7 +168,7 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
if name != "" {
return items, nil
}
roots := make([]*biz.Department, 0)
roots := make([]*system.Department, 0)
for _, po := range pos {
n := nodes[po.ID]
if p := nodes[po.ParentID]; p != nil {
+30 -30
View File
@@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"kra/internal/biz/system"
"strings"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -15,7 +15,7 @@ import (
type dictionaryRepo struct{ data Provider }
func NewDictionaryRepo(data Provider) biz.DictionaryRepo { return &dictionaryRepo{data: data} }
func NewDictionaryRepo(data Provider) system.DictionaryRepo { return &dictionaryRepo{data: data} }
type dictionaryPO struct {
ID uint `gorm:"primaryKey"`
@@ -49,17 +49,17 @@ type dictionaryDetailPO struct {
func (dictionaryDetailPO) TableName() string { return "sys_dictionary_details" }
func dictionaryFromPO(po dictionaryPO) *biz.Dictionary {
return &biz.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
func dictionaryFromPO(po dictionaryPO) *system.Dictionary {
return &system.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
}
func detailFromPO(po dictionaryDetailPO) *biz.DictionaryDetail {
return &biz.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
func detailFromPO(po dictionaryDetailPO) *system.DictionaryDetail {
return &system.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
}
func parameterFromPO(po parameterPO) *biz.SystemParameter {
return &biz.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
func parameterFromPO(po parameterPO) *system.SystemParameter {
return &system.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
}
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *system.Dictionary) error {
var existing dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("type = ?", v.Type).First(&existing).Error; err == nil {
return errors.New("存在相同的type,不允许创建")
@@ -75,7 +75,7 @@ func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary
return nil
}
func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error {
func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *system.Dictionary, details []*system.DictionaryDetail) error {
var existing dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("type = ?", dictionary.Type).First(&existing).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("存在相同的type,不允许导入")
@@ -119,7 +119,7 @@ func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.D
return nil
})
}
func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *system.Dictionary) error {
db := r.data.DB().WithContext(ctx)
var current dictionaryPO
if err := db.Where("id = ?", v.ID).First(&current).Error; err != nil {
@@ -175,7 +175,7 @@ func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
}
return db.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error
}
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*system.Dictionary, error) {
var po dictionaryPO
db := r.data.DB().WithContext(ctx)
active := true
@@ -192,7 +192,7 @@ func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string
if err = db.Where("sys_dictionary_id = ? AND status = ?", out.ID, true).Order("sort").Find(&detailPOs).Error; err != nil {
return nil, err
}
out.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs))
out.Details = make([]*system.DictionaryDetail, 0, len(detailPOs))
for _, detail := range detailPOs {
out.Details = append(out.Details, detailFromPO(detail))
}
@@ -200,7 +200,7 @@ func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string
return out, err
}
func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) {
func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*system.Dictionary, error) {
var po dictionaryPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
@@ -210,13 +210,13 @@ func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Di
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", id).Order("sort").Find(&detailPOs).Error; err != nil {
return nil, err
}
value.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs))
value.Details = make([]*system.DictionaryDetail, 0, len(detailPOs))
for _, detail := range detailPOs {
value.Details = append(value.Details, detailFromPO(detail))
}
return value, nil
}
func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) {
func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*system.Dictionary, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&dictionaryPO{})
if name != "" {
like := "%" + name + "%"
@@ -265,16 +265,16 @@ func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, n
detailsByDictionary[value.DictionaryID] = append(detailsByDictionary[value.DictionaryID], value)
}
}
out := make([]*biz.Dictionary, 0, len(pos))
out := make([]*system.Dictionary, 0, len(pos))
for _, po := range pos {
v := dictionaryFromPO(po)
if details {
v.Details = make([]*biz.DictionaryDetail, 0, len(detailsByDictionary[v.ID]))
v.Details = make([]*system.DictionaryDetail, 0, len(detailsByDictionary[v.ID]))
for _, detail := range detailsByDictionary[v.ID] {
v.Details = append(v.Details, detailFromPO(detail))
}
} else {
v.Children = make([]*biz.Dictionary, 0, len(childrenByParent[po.ID]))
v.Children = make([]*system.Dictionary, 0, len(childrenByParent[po.ID]))
for _, child := range childrenByParent[po.ID] {
v.Children = append(v.Children, dictionaryFromPO(child))
}
@@ -284,7 +284,7 @@ func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, n
return out, total, nil
}
func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *system.DictionaryDetail) error {
po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID}
po.Level = 0
po.Path = ""
@@ -304,7 +304,7 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
v.Path = po.Path
return nil
}
func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *system.DictionaryDetail) error {
var po dictionaryDetailPO
db := r.data.DB().WithContext(ctx)
if err := db.First(&po, v.ID).Error; err != nil {
@@ -359,14 +359,14 @@ func (r *dictionaryRepo) DeleteDictionaryDetail(ctx context.Context, id uint) er
}
return r.data.DB().WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
}
func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) {
func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*system.DictionaryDetail, error) {
var po dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return detailFromPO(po), nil
}
func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) {
func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter system.DictionaryDetailFilter) ([]*system.DictionaryDetail, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&dictionaryDetailPO{})
if filter.DictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", filter.DictionaryID)
@@ -394,13 +394,13 @@ func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size i
if err := pagination.ApplyRequired(db.Order("sort,id"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DictionaryDetail, 0, len(pos))
out := make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
out = append(out, detailFromPO(po))
}
return out, total, nil
}
func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) {
func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*system.DictionaryDetail, error) {
if dictionaryID == 0 {
// The tree-by-type endpoint resolves only by dictionary type. Unlike the
// public dictionary lookup, it does not require the dictionary itself to
@@ -415,7 +415,7 @@ func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&pos).Error; err != nil {
return nil, err
}
roots := make([]*biz.DictionaryDetail, 0, len(pos))
roots := make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
item := detailFromPO(po)
if err := r.loadDictionaryDetailChildren(ctx, item); err != nil {
@@ -426,7 +426,7 @@ func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID
return roots, nil
}
func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*biz.DictionaryDetail, error) {
func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*system.DictionaryDetail, error) {
db := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", dictionaryID)
if parentID == nil {
db = db.Where("parent_id IS NULL")
@@ -437,7 +437,7 @@ func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictiona
if err := db.Order("sort").Find(&pos).Error; err != nil {
return nil, err
}
items := make([]*biz.DictionaryDetail, 0, len(pos))
items := make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
item := detailFromPO(po)
if includeChildren {
@@ -450,12 +450,12 @@ func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictiona
return items, nil
}
func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *biz.DictionaryDetail) error {
func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *system.DictionaryDetail) error {
var pos []dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).Where("parent_id = ?", parent.ID).Order("sort").Find(&pos).Error; err != nil {
return err
}
parent.Children = make([]*biz.DictionaryDetail, 0, len(pos))
parent.Children = make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
child := detailFromPO(po)
if err := r.loadDictionaryDetailChildren(ctx, child); err != nil {
@@ -2,9 +2,8 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
)
func TestDictionaryListPreservesPreloadCollectionShapes(t *testing.T) {
@@ -12,10 +11,10 @@ func TestDictionaryListPreservesPreloadCollectionShapes(t *testing.T) {
ctx := context.Background()
repo := &dictionaryRepo{data: data}
if err := repo.CreateDictionary(ctx, &biz.Dictionary{Name: "status", Type: "status"}); err != nil {
if err := repo.CreateDictionary(ctx, &system.Dictionary{Name: "status", Type: "status"}); err != nil {
t.Fatal(err)
}
if err := repo.CreateDictionary(ctx, &biz.Dictionary{Name: "kind", Type: "kind"}); err != nil {
if err := repo.CreateDictionary(ctx, &system.Dictionary{Name: "kind", Type: "kind"}); err != nil {
t.Fatal(err)
}
@@ -47,10 +46,10 @@ func TestImportDictionaryRejectsDuplicateType(t *testing.T) {
ctx := context.Background()
repo := &dictionaryRepo{data: data}
if err := repo.CreateDictionary(ctx, &biz.Dictionary{Name: "first", Type: "duplicate"}); err != nil {
if err := repo.CreateDictionary(ctx, &system.Dictionary{Name: "first", Type: "duplicate"}); err != nil {
t.Fatal(err)
}
if err := repo.ImportDictionary(ctx, &biz.Dictionary{Name: "second", Type: "duplicate"}, nil); err == nil || err.Error() != "存在相同的type,不允许导入" {
if err := repo.ImportDictionary(ctx, &system.Dictionary{Name: "second", Type: "duplicate"}, nil); err == nil || err.Error() != "存在相同的type,不允许导入" {
t.Fatalf("duplicate import error = %v", err)
}
@@ -68,16 +67,16 @@ func TestDictionaryTreeAndParentQueriesPreserveLoadedChildrenShape(t *testing.T)
ctx := context.Background()
repo := &dictionaryRepo{data: data}
dictionary := &biz.Dictionary{Name: "tree", Type: "tree"}
dictionary := &system.Dictionary{Name: "tree", Type: "tree"}
if err := repo.CreateDictionary(ctx, dictionary); err != nil {
t.Fatal(err)
}
parent := &biz.DictionaryDetail{Label: "parent", Value: "parent", DictionaryID: dictionary.ID, Sort: 2}
parent := &system.DictionaryDetail{Label: "parent", Value: "parent", DictionaryID: dictionary.ID, Sort: 2}
if err := repo.CreateDictionaryDetail(ctx, parent); err != nil {
t.Fatal(err)
}
parentID := parent.ID
child := &biz.DictionaryDetail{Label: "child", Value: "child", DictionaryID: dictionary.ID, ParentID: &parentID, Sort: 1}
child := &system.DictionaryDetail{Label: "child", Value: "child", DictionaryID: dictionary.ID, ParentID: &parentID, Sort: 1}
if err := repo.CreateDictionaryDetail(ctx, child); err != nil {
t.Fatal(err)
}
@@ -112,15 +111,15 @@ func TestDictionaryDetailListAppliesZeroLimit(t *testing.T) {
ctx := context.Background()
repo := &dictionaryRepo{data: data}
dictionary := &biz.Dictionary{Name: "limited", Type: "limited"}
dictionary := &system.Dictionary{Name: "limited", Type: "limited"}
if err := repo.CreateDictionary(ctx, dictionary); err != nil {
t.Fatal(err)
}
if err := repo.CreateDictionaryDetail(ctx, &biz.DictionaryDetail{Label: "one", Value: "one", DictionaryID: dictionary.ID}); err != nil {
if err := repo.CreateDictionaryDetail(ctx, &system.DictionaryDetail{Label: "one", Value: "one", DictionaryID: dictionary.ID}); err != nil {
t.Fatal(err)
}
items, total, err := repo.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: dictionary.ID})
items, total, err := repo.ListDictionaryDetails(ctx, 0, 0, system.DictionaryDetailFilter{DictionaryID: dictionary.ID})
if err != nil {
t.Fatal(err)
}
@@ -136,7 +135,7 @@ func TestUserListAppliesZeroLimitForNegativePageSize(t *testing.T) {
t.Fatal(err)
}
items, total, err := (&userRepo{data: data}).ListUsers(ctx, 1, -1, &biz.UserListFilter{})
items, total, err := (&userRepo{data: data}).ListUsers(ctx, 1, -1, &system.UserListFilter{})
if err != nil {
t.Fatal(err)
}
@@ -150,7 +149,7 @@ func TestDepartmentTreeAndFlatResultsPreserveChildrenShape(t *testing.T) {
ctx := context.Background()
repo := &departmentRepo{data: data}
root := &biz.Department{Name: "root"}
root := &system.Department{Name: "root"}
if err := repo.CreateDepartment(ctx, root); err != nil {
t.Fatal(err)
}
@@ -158,7 +157,7 @@ func TestDepartmentTreeAndFlatResultsPreserveChildrenShape(t *testing.T) {
if err := data.gormDB.WithContext(ctx).Where("name = ?", "root").First(&rootPO).Error; err != nil {
t.Fatal(err)
}
if err := repo.CreateDepartment(ctx, &biz.Department{Name: "child", ParentID: rootPO.ID}); err != nil {
if err := repo.CreateDepartment(ctx, &system.Department{Name: "child", ParentID: rootPO.ID}); err != nil {
t.Fatal(err)
}
+8 -8
View File
@@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -26,10 +26,10 @@ type errorRecordPO struct {
func (errorRecordPO) TableName() string { return "sys_error" }
func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
func errorFromPO(v errorRecordPO) *system.ErrorRecord {
return &system.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
}
func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
func (r *auditRecorderRepo) CreateError(ctx context.Context, v *system.ErrorRecord) error {
if !r.data.DatabaseReady() {
// Silently ignore error records before the database is initialized.
return nil
@@ -39,7 +39,7 @@ func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord)
}
return r.data.DB().WithContext(ctx).Create(&errorRecordPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error
}
func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
func (r *auditQueryRepo) UpdateError(ctx context.Context, v *system.ErrorRecord) error {
updates := make(map[string]any, 9)
if v.ID != 0 {
updates["id"] = v.ID
@@ -73,14 +73,14 @@ func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) er
func (r *auditQueryRepo) DeleteErrors(ctx context.Context, ids []uint) error {
return r.data.DB().WithContext(ctx).Delete(&errorRecordPO{}, ids).Error
}
func (r *auditQueryRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) {
func (r *auditQueryRepo) FindError(ctx context.Context, id uint) (*system.ErrorRecord, error) {
var po errorRecordPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return errorFromPO(po), nil
}
func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) {
func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *system.ErrorRecord) ([]*system.ErrorRecord, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&errorRecordPO{})
if q != nil {
if len(q.CreatedAtRange) == 2 {
@@ -101,7 +101,7 @@ func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.
if err := pagination.Apply(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ErrorRecord, 0, len(pos))
out := make([]*system.ErrorRecord, 0, len(pos))
for _, po := range pos {
out = append(out, errorFromPO(po))
}
@@ -2,10 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
)
func errorString(value string) *string { return &value }
@@ -41,7 +40,7 @@ func TestCreateErrorBeforeDatabaseInitializationIsNoop(t *testing.T) {
t.Cleanup(func() { _ = sqlDB.Close() })
recorder := &auditRecorderRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
form := "后端"
if err = recorder.CreateError(context.Background(), &biz.ErrorRecord{Form: &form}); err != nil {
if err = recorder.CreateError(context.Background(), &system.ErrorRecord{Form: &form}); err != nil {
t.Fatalf("uninitialized error write = %v, want nil", err)
}
}
@@ -50,7 +49,7 @@ func TestCreateErrorPreservesNullableFieldsAndDefaultStatus(t *testing.T) {
recorder, query := newErrorRecordRepos(t)
createdAt := time.Date(2025, time.March, 4, 5, 6, 7, 0, time.UTC)
form := "前端"
if err := recorder.CreateError(context.Background(), &biz.ErrorRecord{
if err := recorder.CreateError(context.Background(), &system.ErrorRecord{
ID: 41,
CreatedAt: createdAt,
Form: &form,
@@ -80,7 +79,7 @@ func TestCreateErrorPreservesNullableFieldsAndDefaultStatus(t *testing.T) {
func TestUpdateErrorDistinguishesOmittedAndExplicitEmptyFields(t *testing.T) {
recorder, query := newErrorRecordRepos(t)
form, info, solution := "后端", "错误内容", "解决方案"
if err := recorder.CreateError(context.Background(), &biz.ErrorRecord{
if err := recorder.CreateError(context.Background(), &system.ErrorRecord{
Form: &form,
Info: &info,
Solution: &solution,
@@ -98,7 +97,7 @@ func TestUpdateErrorDistinguishesOmittedAndExplicitEmptyFields(t *testing.T) {
}
id := items[0].ID
newForm := "服务端"
if err = query.UpdateError(context.Background(), &biz.ErrorRecord{ID: id, Form: &newForm}); err != nil {
if err = query.UpdateError(context.Background(), &system.ErrorRecord{ID: id, Form: &newForm}); err != nil {
t.Fatal(err)
}
got, err := query.FindError(context.Background(), id)
@@ -110,7 +109,7 @@ func TestUpdateErrorDistinguishesOmittedAndExplicitEmptyFields(t *testing.T) {
}
changedCreatedAt := time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC)
if err = query.UpdateError(context.Background(), &biz.ErrorRecord{
if err = query.UpdateError(context.Background(), &system.ErrorRecord{
ID: id,
CreatedAt: changedCreatedAt,
Form: errorString(""),
+21 -20
View File
@@ -5,12 +5,13 @@ import (
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz"
"kra/internal/biz/system"
"strconv"
"strings"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type exportTemplatePO struct {
@@ -59,27 +60,27 @@ func (exportJoinPO) TableName() string { return "sys_export_template_join" }
type exportRepo struct{ data Provider }
func NewExportRepo(data Provider) biz.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate {
v := &biz.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
func NewExportRepo(data Provider) system.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *system.ExportTemplate {
v := &system.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
if conditions != nil {
v.Conditions = make([]biz.ExportCondition, 0, len(conditions))
v.Conditions = make([]system.ExportCondition, 0, len(conditions))
}
for _, x := range conditions {
v.Conditions = append(v.Conditions, biz.ExportCondition{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
v.Conditions = append(v.Conditions, system.ExportCondition{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
}
if joins != nil {
v.Joins = make([]biz.ExportJoin, 0, len(joins))
v.Joins = make([]system.ExportJoin, 0, len(joins))
}
for _, x := range joins {
v.Joins = append(v.Joins, biz.ExportJoin{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
v.Joins = append(v.Joins, system.ExportJoin{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
}
return v
}
func exportToPO(v *biz.ExportTemplate) exportTemplatePO {
func exportToPO(v *system.ExportTemplate) exportTemplatePO {
return exportTemplatePO{ID: v.ID, DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
}
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs, replace, forceTemplateID bool) error {
func (r *exportRepo) saveRelations(tx *gorm.DB, v *system.ExportTemplate, resetIDs, replace, forceTemplateID bool) error {
if replace {
if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil {
return err
@@ -137,7 +138,7 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs,
}
return nil
}
func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *system.ExportTemplate) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v)
if err := tx.Create(&po).Error; err != nil {
@@ -147,7 +148,7 @@ func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemp
return r.saveRelations(tx, v, false, false, true)
})
}
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *system.ExportTemplate) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v)
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
@@ -159,7 +160,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
return r.data.DB().WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).Error
}
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*system.ExportTemplate, error) {
var po exportTemplatePO
db := r.data.DB().WithContext(ctx)
var err error
@@ -181,7 +182,7 @@ func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string
}
return exportFromPO(po, conditions, joins), nil
}
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) {
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *system.ExportTemplate) ([]*system.ExportTemplate, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&exportTemplatePO{})
if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
@@ -210,14 +211,14 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
if err := db.Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ExportTemplate, 0, len(pos))
out := make([]*system.ExportTemplate, 0, len(pos))
for _, po := range pos {
out = append(out, exportFromPO(po, nil, nil))
}
return out, total, nil
}
func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
func (r *exportRepo) QueryExport(ctx context.Context, t *system.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
selected, err := r.data.Database(t.DBName)
if err != nil {
return nil, "", err
@@ -327,7 +328,7 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
return rows, preview, err
}
func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) (string, error) {
func (r *exportRepo) PreviewExport(ctx context.Context, t *system.ExportTemplate, params map[string]string) (string, error) {
columns := make([]string, 0)
decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
@@ -437,7 +438,7 @@ func parseTemplateColumns(raw string) map[string]string {
}
return out
}
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
func (r *exportRepo) ImportExportRows(ctx context.Context, t *system.ExportTemplate, rows []map[string]any) error {
// ImportSQL is checked verbatim. In particular, a whitespace-only value
// is still treated as custom SQL and is allowed to return the driver's
// native error instead of silently falling back to GORM insertion.
+11 -11
View File
@@ -4,10 +4,10 @@ import (
"context"
"encoding/json"
"errors"
"kra/internal/biz/system"
"strings"
"time"
"kra/internal/biz"
"kra/internal/integration/runtimeconfig"
"gorm.io/gorm"
@@ -31,23 +31,23 @@ type integrationRuntimeProvider interface {
IntegrationRuntime() *runtimeconfig.Store
}
func NewIntegrationConfigRepo(data Provider) biz.IntegrationConfigRepo {
func NewIntegrationConfigRepo(data Provider) system.IntegrationConfigRepo {
return &integrationConfigRepo{data: data}
}
func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*biz.IntegrationConfig, error) {
func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*system.IntegrationConfig, error) {
var rows []integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ?", kind).Order("provider ASC").Find(&rows).Error; err != nil {
return nil, err
}
result := make([]*biz.IntegrationConfig, 0, len(rows))
result := make([]*system.IntegrationConfig, 0, len(rows))
for _, row := range rows {
result = append(result, integrationConfigFromPO(row))
}
return result, nil
}
func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*biz.IntegrationConfig, error) {
func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*system.IntegrationConfig, error) {
var row integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -58,14 +58,14 @@ func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind,
return integrationConfigFromPO(row), nil
}
func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *biz.IntegrationConfig) error {
func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *system.IntegrationConfig) error {
db := r.data.DB().WithContext(ctx)
var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", config.Kind, config.Provider).First(&row).Error
values := integrationObject(config.Values)
if errors.Is(err, gorm.ErrRecordNotFound) {
if config.Enabled {
if err = biz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
if err = system.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
return err
}
}
@@ -81,7 +81,7 @@ func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, confi
}
mergeIntegrationSecrets(config.Kind, config.Provider, values, integrationObject(json.RawMessage(row.Config)))
if config.Enabled {
if err = biz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
if err = system.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
return err
}
}
@@ -116,11 +116,11 @@ func integrationRuntime(provider Provider) *runtimeconfig.Store {
return nil
}
func integrationConfigFromPO(row integrationConfigPO) *biz.IntegrationConfig {
func integrationConfigFromPO(row integrationConfigPO) *system.IntegrationConfig {
values := integrationObject(json.RawMessage(row.Config))
maskIntegrationSecrets(row.Kind, row.Provider, values)
encoded, _ := json.Marshal(values)
return &biz.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded}
return &system.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded}
}
func integrationObject(raw json.RawMessage) map[string]any {
@@ -165,7 +165,7 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any)
func integrationSecretFields(kind, provider string) map[string]bool {
result := map[string]bool{}
if definition, ok := biz.IntegrationDefinition(kind, provider); ok {
if definition, ok := system.IntegrationDefinition(kind, provider); ok {
for _, field := range definition.Fields {
if field.Secret {
result[field.Key] = true
@@ -3,9 +3,9 @@ package system
import (
"context"
"encoding/json"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"kra/internal/integration/runtimeconfig"
)
@@ -27,19 +27,19 @@ func TestIntegrationConfigSavePublishesUnmaskedRuntimeValues(t *testing.T) {
provider := &integrationRuntimeTestProvider{Data: &Data{gormDB: newReloadableDB(db, nil)}, store: runtimeconfig.NewStore()}
repo := &integrationConfigRepo{data: provider}
values := biz.DefaultIntegrationConfig(biz.IntegrationKindMQ, "rabbitmq")
values := system.DefaultIntegrationConfig(system.IntegrationKindMQ, "rabbitmq")
values["password"] = "runtime-secret"
raw, _ := json.Marshal(values)
if err = repo.SaveIntegrationConfig(context.Background(), &biz.IntegrationConfig{Kind: biz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
if err = repo.SaveIntegrationConfig(context.Background(), &system.IntegrationConfig{Kind: system.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
t.Fatal(err)
}
values["password"] = "******"
raw, _ = json.Marshal(values)
if err = repo.SaveIntegrationConfig(context.Background(), &biz.IntegrationConfig{Kind: biz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
if err = repo.SaveIntegrationConfig(context.Background(), &system.IntegrationConfig{Kind: system.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
t.Fatal(err)
}
active, ok := provider.store.Get(biz.IntegrationKindMQ, "rabbitmq")
active, ok := provider.store.Get(system.IntegrationKindMQ, "rabbitmq")
if !ok || !active.Enabled {
t.Fatalf("runtime config = %#v, ok=%v", active, ok)
}
+47 -48
View File
@@ -7,14 +7,13 @@ import (
"fmt"
"io"
"io/fs"
"kra/internal/biz/system"
"os"
pathpkg "path"
"path/filepath"
"sort"
"strings"
"time"
"kra/internal/biz"
)
const (
@@ -26,21 +25,21 @@ const (
func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) {
admin := r.data.Runtime().Admin()
if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" {
return "", false, biz.ErrLogRootUnavailable
return "", false, system.ErrLogRootUnavailable
}
root, err = filepath.Abs(admin.Zap.Director)
if err != nil {
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
info, err := os.Stat(root)
if errors.Is(err, fs.ErrNotExist) {
return root, false, nil
}
if err != nil {
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
if !info.IsDir() {
return "", false, biz.ErrLogRootUnavailable
return "", false, system.ErrLogRootUnavailable
}
return root, true, nil
}
@@ -52,12 +51,12 @@ func (r *logFileRepo) openConfiguredLogRoot() (root *os.Root, exists bool, err e
}
root, err = os.OpenRoot(rootPath)
if err != nil {
return nil, false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, false, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
return root, true, nil
}
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]system.LogDate, error) {
if err := validateLogMonth(month); err != nil {
return nil, err
}
@@ -66,15 +65,15 @@ func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate
return nil, err
}
if !exists {
return []biz.LogDate{}, nil
return []system.LogDate{}, nil
}
defer logRoot.Close()
entries, err := fs.ReadDir(logRoot.FS(), ".")
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
result := make([]biz.LogDate, 0)
result := make([]system.LogDate, 0)
for _, entry := range entries {
if err = ctx.Err(); err != nil {
return nil, err
@@ -90,14 +89,14 @@ func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate
return nil, countErr
}
if count > 0 {
result = append(result, biz.LogDate{Date: entry.Name(), FileCount: count})
result = append(result, system.LogDate{Date: entry.Name(), FileCount: count})
}
}
sort.Slice(result, func(i, j int) bool { return result[i].Date < result[j].Date })
return result, nil
}
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]system.LogFile, error) {
if err := validateLogDate(date); err != nil {
return nil, err
}
@@ -106,27 +105,27 @@ func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile,
return nil, err
}
if !exists {
return []biz.LogFile{}, nil
return []system.LogFile{}, nil
}
defer logRoot.Close()
info, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return []biz.LogFile{}, nil
return []system.LogFile{}, nil
}
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return []biz.LogFile{}, nil
return []system.LogFile{}, nil
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
defer dateRoot.Close()
result := make([]biz.LogFile, 0)
result := make([]system.LogFile, 0)
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
@@ -151,24 +150,24 @@ func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile,
return infoErr
}
if fileInfo.Mode().IsRegular() {
result = append(result, biz.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()})
result = append(result, system.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path })
return result, nil
}
func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*biz.LogContent, error) {
result := &biz.LogContent{Date: date, Path: apiPath}
func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*system.LogContent, error) {
result := &system.LogContent{Date: date, Path: apiPath}
if err := ctx.Err(); err != nil {
return nil, err
}
if cursor != nil && *cursor < 0 {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
file, info, err := r.openValidatedLogFile(date, apiPath)
if err != nil {
@@ -182,19 +181,19 @@ func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, curs
}
start, limitedByBytes, err := findLogChunkStart(file, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
data, err := readLogRange(file, start, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
if int64(len(data)) < end-start {
currentInfo, statErr := file.Stat()
if statErr != nil || currentInfo.Size() < start {
if statErr != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
return nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, statErr)
}
return nil, biz.ErrLogFileUnreadable
return nil, system.ErrLogFileUnreadable
}
info = currentInfo
}
@@ -221,23 +220,23 @@ func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File,
return nil, nil, err
}
if !exists {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
defer logRoot.Close()
dateInfo, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
if dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
defer dateRoot.Close()
@@ -247,35 +246,35 @@ func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File,
relativePath = filepath.Join(relativePath, segment)
validatedInfo, err = dateRoot.Lstat(relativePath)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
if validatedInfo.Mode()&os.ModeSymlink != 0 {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
if index < len(segments)-1 && !validatedInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
}
if validatedInfo == nil || !validatedInfo.Mode().IsRegular() {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
file, err = dateRoot.Open(relativePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
openedInfo, statErr := file.Stat()
if statErr != nil || !openedInfo.Mode().IsRegular() || !os.SameFile(validatedInfo, openedInfo) {
file.Close()
if statErr != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, statErr)
}
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
return file, openedInfo, nil
}
@@ -283,7 +282,7 @@ func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File,
func validateLogMonth(month string) error {
parsed, err := time.Parse("2006-01", month)
if err != nil || parsed.Format("2006-01") != month {
return biz.ErrInvalidLogMonth
return system.ErrInvalidLogMonth
}
return nil
}
@@ -291,23 +290,23 @@ func validateLogMonth(month string) error {
func validateLogDate(date string) error {
parsed, err := time.Parse("2006-01-02", date)
if err != nil || parsed.Format("2006-01-02") != date {
return biz.ErrInvalidLogDate
return system.ErrInvalidLogDate
}
return nil
}
func validateLogAPIPath(apiPath string) ([]string, error) {
if apiPath == "" || strings.Contains(apiPath, "\\") || strings.Contains(apiPath, ":") || pathpkg.IsAbs(apiPath) {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
segments := strings.Split(apiPath, "/")
for _, segment := range segments {
if segment == "" || segment == "." || segment == ".." {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
}
if !strings.EqualFold(pathpkg.Ext(apiPath), ".log") {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
return segments, nil
}
@@ -351,7 +350,7 @@ func countLogFiles(ctx context.Context, logRoot *os.Root, date string) (count in
return nil
})
if err != nil {
return 0, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return 0, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
return count, nil
}
+2 -2
View File
@@ -3,11 +3,11 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"os"
"path/filepath"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@@ -44,7 +44,7 @@ func TestLogViewerRejectsPathTraversal(t *testing.T) {
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_Zap{Director: t.TempDir()}})
repo := &logFileRepo{data: &Data{runtime: runtime}}
_, err := repo.LogContent(context.Background(), "2026-08-16", "../application.log", nil)
if !errors.Is(err, biz.ErrInvalidLogPath) {
if !errors.Is(err, system.ErrInvalidLogPath) {
t.Fatalf("expected invalid path error, got %v", err)
}
}
+7 -7
View File
@@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -23,7 +23,7 @@ type loginLogPO struct {
func (loginLogPO) TableName() string { return "sys_login_logs" }
func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *system.LoginLog) error {
if !r.data.DatabaseReady() {
// The login endpoint remains reachable before database initialization;
// skip the audit write until storage is ready.
@@ -31,10 +31,10 @@ func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) er
}
return r.data.DB().WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error
}
func loginFromPO(v loginLogPO) *biz.LoginLog {
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
func loginFromPO(v loginLogPO) *system.LoginLog {
return &system.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
}
func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *system.LoginLog) ([]*system.LoginLog, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&loginLogPO{})
if q != nil {
if q.Username != "" {
@@ -59,7 +59,7 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.
}
}
users := auditUsers(ctx, r.data.DB().WithContext(ctx), ids)
out := make([]*biz.LoginLog, 0, len(pos))
out := make([]*system.LoginLog, 0, len(pos))
for _, po := range pos {
value := loginFromPO(po)
value.User = users[po.UserID]
@@ -67,7 +67,7 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.
}
return out, total, nil
}
func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) {
func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*system.LoginLog, error) {
var po loginLogPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
+16 -17
View File
@@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
@@ -41,14 +40,14 @@ func (categoryPO) TableName() string { return "media_attachment_category" }
type mediaRepo struct{ data Provider }
func NewMediaRepo(data Provider) biz.MediaRepo { return &mediaRepo{data: data} }
func mediaFromPO(v mediaPO) *biz.MediaFile {
return &biz.MediaFile{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
func NewMediaRepo(data Provider) system.MediaRepo { return &mediaRepo{data: data} }
func mediaFromPO(v mediaPO) *system.MediaFile {
return &system.MediaFile{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
}
func mediaToPO(v *biz.MediaFile) mediaPO {
func mediaToPO(v *system.MediaFile) mediaPO {
return mediaPO{ID: v.ID, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
}
func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
func (r *mediaRepo) CreateMedia(ctx context.Context, v *system.MediaFile) error {
po := mediaToPO(v)
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@@ -58,21 +57,21 @@ func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
v.UpdatedAt = po.UpdatedAt
return nil
}
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*biz.MediaFile, error) {
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*system.MediaFile, error) {
var po mediaPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return mediaFromPO(po), nil
}
func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*biz.MediaFile, error) {
func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*system.MediaFile, error) {
var po mediaPO
if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND md5 = ?", userID, hash).First(&po).Error; err != nil {
return nil, err
}
return mediaFromPO(po), nil
}
func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*biz.MediaFile, int64, error) {
func (r *mediaRepo) ListMedia(ctx context.Context, filter system.MediaFilter) ([]*system.MediaFile, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&mediaPO{})
if filter.Keyword != "" {
db = db.Where("name LIKE ?", "%"+filter.Keyword+"%")
@@ -119,7 +118,7 @@ func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*b
if err := db.Limit(limit).Offset(offset).Order(orderKey).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.MediaFile, 0, len(pos))
out := make([]*system.MediaFile, 0, len(pos))
for _, po := range pos {
out = append(out, mediaFromPO(po))
}
@@ -142,14 +141,14 @@ func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64,
err := r.data.DB().WithContext(ctx).Model(&mediaPO{}).Where(map[string]any{"key": key}).Count(&count).Error
return count, err
}
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error {
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*system.MediaFile) error {
pos := make([]mediaPO, 0, len(items))
for _, v := range items {
pos = append(pos, mediaToPO(v))
}
return r.data.DB().WithContext(ctx).Create(&pos).Error
}
func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory) error {
func (r *mediaRepo) SaveCategory(ctx context.Context, v *system.AttachmentCategory) error {
db := r.data.DB().WithContext(ctx)
var count int64
query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID)
@@ -179,17 +178,17 @@ func (r *mediaRepo) DeleteCategory(ctx context.Context, id uint) error {
}
return r.data.DB().WithContext(ctx).Unscoped().Delete(&categoryPO{}, id).Error
}
func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCategory, error) {
func (r *mediaRepo) ListCategories(ctx context.Context) ([]*system.AttachmentCategory, error) {
var pos []categoryPO
if err := r.data.DB().WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err
}
byID := map[uint]*biz.AttachmentCategory{}
byID := map[uint]*system.AttachmentCategory{}
for _, po := range pos {
// Children remains nil for leaf nodes, which serializes as null.
byID[po.ID] = &biz.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID}
byID[po.ID] = &system.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID}
}
roots := []*biz.AttachmentCategory{}
roots := []*system.AttachmentCategory{}
for _, po := range pos {
v := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
+3 -4
View File
@@ -2,20 +2,19 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
)
func TestMediaListKeepsZeroPageSizeContract(t *testing.T) {
data := newTransactionTestData(t)
repo := &mediaRepo{data: data}
ctx := context.Background()
if err := repo.CreateMedia(ctx, &biz.MediaFile{Name: "one.txt"}); err != nil {
if err := repo.CreateMedia(ctx, &system.MediaFile{Name: "one.txt"}); err != nil {
t.Fatal(err)
}
items, total, err := repo.ListMedia(ctx, biz.MediaFilter{})
items, total, err := repo.ListMedia(ctx, system.MediaFilter{})
if err != nil {
t.Fatal(err)
}
+13 -14
View File
@@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -42,30 +41,30 @@ type uploadChunkPO struct {
func (uploadChunkPO) TableName() string { return "media_upload_chunks" }
func uploadFromPO(v uploadSessionPO) *biz.UploadSession {
return &biz.UploadSession{ID: v.ID, UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status, StorageKey: v.StorageKey, MediaID: v.MediaID}
func uploadFromPO(v uploadSessionPO) *system.UploadSession {
return &system.UploadSession{ID: v.ID, UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status, StorageKey: v.StorageKey, MediaID: v.MediaID}
}
func (r *mediaRepo) FindCompletedSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
func (r *mediaRepo) FindCompletedSession(ctx context.Context, userID uint, hash string) (*system.UploadSession, error) {
var po uploadSessionPO
if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "completed").First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUploadSessionNotFound
return nil, system.ErrUploadSessionNotFound
}
return nil, err
}
return uploadFromPO(po), nil
}
func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*system.UploadSession, error) {
var po uploadSessionPO
if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "uploading").First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUploadSessionNotFound
return nil, system.ErrUploadSessionNotFound
}
return nil, err
}
return uploadFromPO(po), nil
}
func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSession) error {
func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *system.UploadSession) error {
po := uploadSessionPO{UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@@ -73,7 +72,7 @@ func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSessio
v.ID = po.ID
return nil
}
func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*biz.UploadSession, error) {
func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*system.UploadSession, error) {
var po uploadSessionPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
@@ -95,21 +94,21 @@ func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
// for audit/recovery rather than physically removing it.
return r.data.DB().WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
}
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *system.UploadChunk) error {
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
return r.data.DB().WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}},
DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at", "deleted_at"}),
}).Create(&po).Error
}
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*system.UploadChunk, error) {
var pos []uploadChunkPO
if err := r.data.DB().WithContext(ctx).Where("upload_id = ?", uploadID).Order("chunk_index").Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.UploadChunk, 0, len(pos))
out := make([]*system.UploadChunk, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.UploadChunk{Index: po.ChunkIndex, Hash: po.ChunkHash, Size: po.Size})
out = append(out, &system.UploadChunk{Index: po.ChunkIndex, Hash: po.ChunkHash, Size: po.Size})
}
return out, nil
}
+25 -26
View File
@@ -3,31 +3,30 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type menuRepo struct{ data Provider }
func NewMenuRepo(data Provider) biz.MenuRepo { return &menuRepo{data: data} }
func NewMenuRepo(data Provider) system.MenuRepo { return &menuRepo{data: data} }
func menuFromPO(po menuPO) *biz.Menu {
func menuFromPO(po menuPO) *system.Menu {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
return &system.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
}
func menuToPO(v *biz.Menu) menuPO {
func menuToPO(v *system.Menu) menuPO {
return menuPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, ParentID: v.ParentID, Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.ActiveName, KeepAlive: v.KeepAlive, DefaultMenu: v.DefaultMenu, Title: v.Title, Icon: v.Icon, CloseTab: v.CloseTab, TransitionType: v.TransitionType}
}
func replaceMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
func replaceMenuRelations(tx *gorm.DB, menu *system.Menu) error {
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuParameterPO{}).Error; err != nil {
return err
}
@@ -37,7 +36,7 @@ func replaceMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
return createMenuRelations(tx, menu)
}
func createMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
func createMenuRelations(tx *gorm.DB, menu *system.Menu) error {
parameters := make([]menuParameterPO, 0, len(menu.Parameters))
for _, parameter := range menu.Parameters {
parameters = append(parameters, menuParameterPO{ID: parameter.ID, CreatedAt: parameter.CreatedAt, UpdatedAt: parameter.UpdatedAt, MenuID: menu.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
@@ -57,7 +56,7 @@ func createMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
return nil
}
func (r *menuRepo) Create(ctx context.Context, v *biz.Menu) error {
func (r *menuRepo) Create(ctx context.Context, v *system.Menu) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&menuPO{}).Where("name = ?", v.Name).Count(&count).Error; err != nil {
@@ -98,7 +97,7 @@ func (r *menuRepo) Create(ctx context.Context, v *biz.Menu) error {
})
}
func (r *menuRepo) Update(ctx context.Context, v *biz.Menu) error {
func (r *menuRepo) Update(ctx context.Context, v *system.Menu) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var old menuPO
if err := tx.First(&old, v.ID).Error; err != nil {
@@ -156,12 +155,12 @@ func (r *menuRepo) Delete(ctx context.Context, id uint) error {
})
}
func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
func (r *menuRepo) loadRelations(ctx context.Context, menu *system.Menu) error {
var parameters []menuParameterPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&parameters).Error; err != nil {
return err
}
menu.Parameters = make([]*biz.MenuParameter, 0, len(parameters))
menu.Parameters = make([]*system.MenuParameter, 0, len(parameters))
for _, parameter := range parameters {
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
}
@@ -169,14 +168,14 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&buttons).Error; err != nil {
return err
}
menu.Buttons = make([]*biz.MenuButton, 0, len(buttons))
menu.Buttons = make([]*system.MenuButton, 0, len(buttons))
for _, button := range buttons {
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
}
return nil
}
func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
func (r *menuRepo) Find(ctx context.Context, id uint) (*system.Menu, error) {
var po menuPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
@@ -188,11 +187,11 @@ func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
return menu, nil
}
func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
func (r *menuRepo) List(ctx context.Context) ([]*system.Menu, error) {
var pos []menuPO
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).Order("sort")
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
if actor, ok := system.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, err
@@ -204,20 +203,20 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Menu, 0, len(pos))
out := make([]*system.Menu, 0, len(pos))
for _, po := range pos {
out = append(out, menuFromPO(po))
}
if len(out) == 0 {
return out, nil
}
byID := make(map[uint]*biz.Menu, len(out))
byID := make(map[uint]*system.Menu, len(out))
ids := make([]uint, 0, len(out))
for _, menu := range out {
byID[menu.ID] = menu
ids = append(ids, menu.ID)
menu.Parameters = []*biz.MenuParameter{}
menu.Buttons = []*biz.MenuButton{}
menu.Parameters = []*system.MenuParameter{}
menu.Buttons = []*system.MenuButton{}
}
var parameters []menuParameterPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&parameters).Error; err != nil {
@@ -238,22 +237,22 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
return out, nil
}
func menuParameterFromPO(po menuParameterPO) *biz.MenuParameter {
func menuParameterFromPO(po menuParameterPO) *system.MenuParameter {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
return &system.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
}
func menuButtonFromPO(po menuButtonPO) *biz.MenuButton {
func menuButtonFromPO(po menuButtonPO) *system.MenuButton {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
return &system.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
}
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
@@ -292,7 +291,7 @@ func (r *menuRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error
// menus. In particular, it deliberately does not call List: List applies the
// current request actor's strict-auth filter, which would incorrectly hide
// menus when an administrator inspects a different (child) authority.
func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*system.Menu, error) {
var pos []menuPO
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).
Where("id IN (?)", r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).
@@ -301,7 +300,7 @@ func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Menu, 0, len(pos))
out := make([]*system.Menu, 0, len(pos))
for _, po := range pos {
// This endpoint does not preload Parameters or MenuBtn. Keep
// both relations nil so the response shape remains compatible.
+6 -6
View File
@@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
"kra/internal/conf"
)
@@ -31,7 +31,7 @@ func TestMenuCreatePreservesSubmittedTimestamps(t *testing.T) {
ctx := context.Background()
createdAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC)
updatedAt := time.Date(2025, time.February, 3, 4, 5, 6, 0, time.UTC)
menu := &biz.Menu{CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "created", Path: "created", Component: "view/created.vue", Title: "Created"}
menu := &system.Menu{CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "created", Path: "created", Component: "view/created.vue", Title: "Created"}
if err := (&menuRepo{data: data}).Create(ctx, menu); err != nil {
t.Fatal(err)
@@ -90,16 +90,16 @@ func TestMenuUpdateRebuildsRelationsWithoutChangingRequestedIdentity(t *testing.
}
repo := &menuRepo{data: data}
err := repo.Update(ctx, &biz.Menu{
err := repo.Update(ctx, &system.Menu{
ID: 10,
Name: "after",
Path: "after",
Component: "view/after.vue",
Title: "After",
Parameters: []*biz.MenuParameter{{
Parameters: []*system.MenuParameter{{
ID: 21, CreatedAt: createdAt, UpdatedAt: updatedAt, Type: "params", Key: "new", Value: "value",
}},
Buttons: []*biz.MenuButton{{
Buttons: []*system.MenuButton{{
ID: 31, CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "edit", Description: "new",
}},
})
@@ -163,7 +163,7 @@ func TestSetMenuRolesStrictOnlyChangesManagedAuthorities(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &menuRepo{data: data}
if err := repo.SetMenuRoles(ctx, menu.ID, []uint{siblingID}); err == nil {
t.Fatal("SetMenuRoles() accepted an out-of-scope authority")
+2 -3
View File
@@ -1,10 +1,9 @@
package system
import (
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
@@ -98,4 +97,4 @@ func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
type userRepo struct{ data Provider }
func NewUserRepo(data Provider) biz.UserRepo { return &userRepo{data: data} }
func NewUserRepo(data Provider) system.UserRepo { return &userRepo{data: data} }
+10 -10
View File
@@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -30,13 +30,13 @@ type operationPO struct {
func (operationPO) TableName() string { return "sys_operation_records" }
func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *system.OperationRecord) error {
return r.data.DB().WithContext(ctx).Create(&operationPO{IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}).Error
}
func opFromPO(v operationPO) *biz.OperationRecord {
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
func opFromPO(v operationPO) *system.OperationRecord {
return &system.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
}
func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *system.OperationRecord) ([]*system.OperationRecord, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&operationPO{})
if q != nil {
if q.Path != "" {
@@ -58,7 +58,7 @@ func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *
return nil, 0, err
}
users := auditUsers(ctx, r.data.DB().WithContext(ctx), operationUserIDs(pos))
out := make([]*biz.OperationRecord, 0, len(pos))
out := make([]*system.OperationRecord, 0, len(pos))
for _, po := range pos {
value := opFromPO(po)
value.User = users[po.UserID]
@@ -77,8 +77,8 @@ func operationUserIDs(values []operationPO) []uint {
return ids
}
func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User {
result := make(map[uint]*biz.User)
func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*system.User {
result := make(map[uint]*system.User)
if len(ids) == 0 {
return result
}
@@ -87,11 +87,11 @@ func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User
return result
}
for _, value := range users {
result[value.ID] = &biz.User{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, UUID: value.UUID, Username: value.Username, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, DeptID: value.DeptID, Phone: value.Phone, Email: value.Email, Enable: value.Enable}
result[value.ID] = &system.User{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, UUID: value.UUID, Username: value.Username, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, DeptID: value.DeptID, Phone: value.Phone, Email: value.Email, Enable: value.Enable}
}
return result
}
func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) {
func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*system.OperationRecord, error) {
var po operationPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
+8 -8
View File
@@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@@ -13,7 +13,7 @@ import (
type parameterRepo struct{ data Provider }
func NewParameterRepo(data Provider) biz.ParameterRepo { return &parameterRepo{data: data} }
func NewParameterRepo(data Provider) system.ParameterRepo { return &parameterRepo{data: data} }
type parameterPO struct {
ID uint `gorm:"primaryKey"`
@@ -28,7 +28,7 @@ type parameterPO struct {
func (parameterPO) TableName() string { return "sys_params" }
func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error {
func (r *parameterRepo) CreateParameter(ctx context.Context, v *system.SystemParameter) error {
po := parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@@ -36,7 +36,7 @@ func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParame
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
return nil
}
func (r *parameterRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
func (r *parameterRepo) UpdateParameter(ctx context.Context, v *system.SystemParameter) error {
// The compatible update uses a struct, so zero-value optional fields (notably desc)
// are intentionally ignored rather than clearing an existing value.
return r.data.DB().WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(&parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}).Error
@@ -47,21 +47,21 @@ func (r *parameterRepo) DeleteParameters(ctx context.Context, ids []string) erro
}
return r.data.DB().WithContext(ctx).Delete(&[]parameterPO{}, "id in ?", ids).Error
}
func (r *parameterRepo) FindParameterByID(ctx context.Context, id string) (*biz.SystemParameter, error) {
func (r *parameterRepo) FindParameterByID(ctx context.Context, id string) (*system.SystemParameter, error) {
var po parameterPO
if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return nil, err
}
return parameterFromPO(po), nil
}
func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*biz.SystemParameter, error) {
func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*system.SystemParameter, error) {
var po parameterPO
if err := r.data.DB().WithContext(ctx).Where(parameterPO{Key: key}).First(&po).Error; err != nil {
return nil, err
}
return parameterFromPO(po), nil
}
func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) {
func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *system.SystemParameter) ([]*system.SystemParameter, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&parameterPO{})
if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
@@ -82,7 +82,7 @@ func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *b
if err := pagination.Apply(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.SystemParameter, 0, len(pos))
out := make([]*system.SystemParameter, 0, len(pos))
for _, po := range pos {
out = append(out, parameterFromPO(po))
}
+3 -4
View File
@@ -2,17 +2,16 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
)
func TestParameterRepositoryPreservesModelMetadata(t *testing.T) {
data := newTransactionTestData(t)
repo := &parameterRepo{data: data}
createdAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.Local)
value := &biz.SystemParameter{ID: 41, CreatedAt: createdAt, Name: "name", Key: "key", Value: "value"}
value := &system.SystemParameter{ID: 41, CreatedAt: createdAt, Name: "name", Key: "key", Value: "value"}
if err := repo.CreateParameter(context.Background(), value); err != nil {
t.Fatal(err)
@@ -40,7 +39,7 @@ func TestParameterRepositoryKeepsReferenceEmptyQuerySemantics(t *testing.T) {
data := newTransactionTestData(t)
repo := &parameterRepo{data: data}
ctx := context.Background()
first := &biz.SystemParameter{Name: "first", Key: "first", Value: "1"}
first := &system.SystemParameter{Name: "first", Key: "first", Value: "1"}
if err := repo.CreateParameter(ctx, first); err != nil {
t.Fatal(err)
}
+4 -5
View File
@@ -3,16 +3,15 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type permissionRepo struct{ data Provider }
func NewPermissionRepo(data Provider) biz.PermissionRepo { return &permissionRepo{data: data} }
func NewPermissionRepo(data Provider) system.PermissionRepo { return &permissionRepo{data: data} }
type menuButtonPO struct {
ID uint `gorm:"primaryKey"`
@@ -34,12 +33,12 @@ type authorityButtonPO struct {
func (authorityButtonPO) TableName() string { return "sys_authority_btns" }
func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) {
func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*system.MenuButton, error) {
var pos []menuButtonPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.MenuButton, 0, len(pos))
out := make([]*system.MenuButton, 0, len(pos))
for _, po := range pos {
out = append(out, menuButtonFromPO(po))
}
+2 -2
View File
@@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@@ -108,7 +108,7 @@ func TestSetSelectedButtonsStrictRequiresManagedRoleAndOwnedButton(t *testing.T)
if err := db.Create(&authorityButtonPO{AuthorityID: actorID, MenuID: 10, ButtonID: 31}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &permissionRepo{data: data}
if err := repo.SetSelectedButtons(ctx, siblingID, 10, []uint{31}); err == nil {
t.Fatal("SetSelectedButtons() accepted an out-of-scope authority")
+9 -10
View File
@@ -3,16 +3,15 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type positionRepo struct{ data Provider }
func NewPositionRepo(data Provider) biz.PositionRepo { return &positionRepo{data: data} }
func NewPositionRepo(data Provider) system.PositionRepo { return &positionRepo{data: data} }
type positionPO struct {
ID uint `gorm:"primaryKey"`
@@ -37,13 +36,13 @@ type userPositionPO struct {
func (userPositionPO) TableName() string { return "sys_user_positions" }
func posFromPO(po positionPO) *biz.Position {
return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
func posFromPO(po positionPO) *system.Position {
return &system.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
}
func (r *positionRepo) CreatePosition(ctx context.Context, v *biz.Position) error {
func (r *positionRepo) CreatePosition(ctx context.Context, v *system.Position) error {
return r.data.DB().WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error
}
func (r *positionRepo) UpdatePosition(ctx context.Context, v *biz.Position) error {
func (r *positionRepo) UpdatePosition(ctx context.Context, v *system.Position) error {
return r.data.DB().WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error
}
func (r *positionRepo) DeletePosition(ctx context.Context, id uint) error {
@@ -59,14 +58,14 @@ func (r *positionRepo) DeletePosition(ctx context.Context, id uint) error {
}
return r.data.DB().WithContext(ctx).Delete(&positionPO{}, id).Error
}
func (r *positionRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) {
func (r *positionRepo) FindPosition(ctx context.Context, id uint) (*system.Position, error) {
var po positionPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return posFromPO(po), nil
}
func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) {
func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *system.PositionListFilter) ([]*system.Position, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&positionPO{})
if q != nil {
if q.Name != "" {
@@ -90,7 +89,7 @@ func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz
if err := db.Order("sort").Limit(size).Offset(size * (page - 1)).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Position, 0, len(pos))
out := make([]*system.Position, 0, len(pos))
for _, po := range pos {
out = append(out, posFromPO(po))
}
+22 -22
View File
@@ -4,9 +4,9 @@ package system
import (
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/security"
@@ -15,7 +15,7 @@ import (
type runtimeSettings struct{ runtime *conf.Runtime }
func NewRuntimeSettings(runtime *conf.Runtime) biz.RuntimeSettings {
func NewRuntimeSettings(runtime *conf.Runtime) system.RuntimeSettings {
return &runtimeSettings{runtime: runtime}
}
@@ -27,8 +27,8 @@ func (s *runtimeSettings) RouterPrefix() string {
return config.RouterPrefix
}
func (s *runtimeSettings) JWTSettings() biz.JWTSettings {
value := biz.JWTSettings{Issuer: "kra", Expires: 7 * 24 * time.Hour, Buffer: 24 * time.Hour}
func (s *runtimeSettings) JWTSettings() system.JWTSettings {
value := system.JWTSettings{Issuer: "kra", Expires: 7 * 24 * time.Hour, Buffer: 24 * time.Hour}
config := s.runtime.Admin()
if config == nil || config.Jwt == nil {
return value
@@ -46,8 +46,8 @@ func (s *runtimeSettings) JWTSettings() biz.JWTSettings {
return value
}
func (s *runtimeSettings) CaptchaSettings() biz.CaptchaSettings {
value := biz.CaptchaSettings{KeyLong: 6, ImageWidth: 240, ImageHeight: 80, StoreExpiration: 3 * time.Minute}
func (s *runtimeSettings) CaptchaSettings() system.CaptchaSettings {
value := system.CaptchaSettings{KeyLong: 6, ImageWidth: 240, ImageHeight: 80, StoreExpiration: 3 * time.Minute}
config := s.runtime.Admin()
if config == nil || config.Captcha == nil {
return value
@@ -67,12 +67,12 @@ func (s *runtimeSettings) CaptchaSettings() biz.CaptchaSettings {
return value
}
func (s *runtimeSettings) MediaSettings() biz.MediaSettings {
func (s *runtimeSettings) MediaSettings() system.MediaSettings {
config := s.runtime.Admin()
if config == nil || config.Media == nil {
return biz.MediaSettings{}
return system.MediaSettings{}
}
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
return system.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
}
func (s *runtimeSettings) UseMultipoint() bool {
@@ -80,13 +80,13 @@ func (s *runtimeSettings) UseMultipoint() bool {
return config != nil && config.System != nil && config.System.UseMultipoint
}
type tokenIssuer struct{ settings biz.RuntimeSettings }
type tokenIssuer struct{ settings system.RuntimeSettings }
func NewTokenIssuer(settings biz.RuntimeSettings) biz.TokenIssuer {
func NewTokenIssuer(settings system.RuntimeSettings) system.TokenIssuer {
return &tokenIssuer{settings: settings}
}
func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePassword bool, expires time.Duration) (*biz.IssuedToken, error) {
func (i *tokenIssuer) IssueToken(user *system.User, authorityID uint, mustChangePassword bool, expires time.Duration) (*system.IssuedToken, error) {
settings := i.settings.JWTSettings()
if expires <= 0 {
expires = settings.Expires
@@ -99,10 +99,10 @@ func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePas
if err != nil {
return nil, err
}
return &biz.IssuedToken{Value: token, ExpiresAt: claims.ExpiresAt.Time, TTL: expires}, nil
return &system.IssuedToken{Value: token, ExpiresAt: claims.ExpiresAt.Time, TTL: expires}, nil
}
func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*biz.IssuedToken, error) {
func (i *tokenIssuer) ReissueToken(source *system.AuthClaims, authorityID uint) (*system.IssuedToken, error) {
if source == nil {
return nil, errors.New("nil JWT claims")
}
@@ -120,23 +120,23 @@ func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*b
if err != nil {
return nil, err
}
return &biz.IssuedToken{Value: token, ExpiresAt: source.ExpiresAt, TTL: time.Until(source.ExpiresAt)}, nil
return &system.IssuedToken{Value: token, ExpiresAt: source.ExpiresAt, TTL: time.Until(source.ExpiresAt)}, nil
}
func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
claims, err := security.Parse(token, i.settings.JWTSettings().SigningKey)
if err != nil {
switch {
case errors.Is(err, security.ErrTokenExpired):
return nil, biz.ErrTokenExpired
return nil, system.ErrTokenExpired
case errors.Is(err, security.ErrTokenMalformed):
return nil, biz.ErrTokenMalformed
return nil, system.ErrTokenMalformed
case errors.Is(err, security.ErrTokenSignatureInvalid):
return nil, biz.ErrTokenSignatureInvalid
return nil, system.ErrTokenSignatureInvalid
case errors.Is(err, security.ErrTokenNotValidYet):
return nil, biz.ErrTokenNotValidYet
return nil, system.ErrTokenNotValidYet
default:
return nil, biz.ErrTokenInvalid
return nil, system.ErrTokenInvalid
}
}
audience := append([]string(nil), claims.Audience...)
@@ -144,5 +144,5 @@ func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
if claims.IssuedAt != nil {
issuedAt = claims.IssuedAt.Time
}
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
return &system.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
}
+7 -8
View File
@@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
@@ -51,17 +50,17 @@ func DefaultSecurityConfig() SecurityConfigPO {
type securityRepo struct{ data DatabaseProvider }
func NewSecurityRepo(data DatabaseProvider) biz.SecurityRepo { return &securityRepo{data: data} }
func NewSecurityRepo(data DatabaseProvider) system.SecurityRepo { return &securityRepo{data: data} }
func securityFromPO(v SecurityConfigPO) *biz.SecurityConfig {
return &biz.SecurityConfig{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
func securityFromPO(v SecurityConfigPO) *system.SecurityConfig {
return &system.SecurityConfig{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func securityToPO(v *biz.SecurityConfig) SecurityConfigPO {
func securityToPO(v *system.SecurityConfig) SecurityConfigPO {
return SecurityConfigPO{ID: 1, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
func (r *securityRepo) SecurityConfig(ctx context.Context) (*system.SecurityConfig, error) {
if !r.data.DatabaseReady() {
po := DefaultSecurityConfig()
po.ID = 0
@@ -80,7 +79,7 @@ func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig,
return securityFromPO(po), nil
}
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *system.SecurityConfig) error {
po := securityToPO(v)
if err := r.data.DB().WithContext(ctx).Save(&po).Error; err != nil {
return err
+4 -4
View File
@@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"strings"
"time"
"kra/internal/biz"
platformmodule "kra/pkg/module"
"github.com/google/uuid"
@@ -13,14 +13,14 @@ import (
"gorm.io/gorm"
)
func SeedSystem(ctx context.Context, db *gorm.DB, input *biz.DatabaseConfig, surfaces ...platformmodule.Surface) error {
func SeedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, surfaces ...platformmodule.Surface) error {
return seedSystem(ctx, db, input, nil, surfaces...)
}
// SeedSystemWithCatalog applies module-contributed administration surfaces and
// default timed tasks in one transaction. The system module remains the owner
// of the system tables, while other modules contribute through the catalog.
func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *biz.DatabaseConfig, catalog platformmodule.Catalog) error {
func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, catalog platformmodule.Catalog) error {
surfaces := make([]platformmodule.Surface, 0, len(catalog.Definitions))
for _, definition := range catalog.Definitions {
surfaces = append(surfaces, definition.Surface)
@@ -28,7 +28,7 @@ func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *biz.Database
return seedSystem(ctx, db, input, catalog.DefaultTimedTasks(), surfaces...)
}
func seedSystem(ctx context.Context, db *gorm.DB, input *biz.DatabaseConfig, defaults []platformmodule.TimedTask, surfaces ...platformmodule.Surface) error {
func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, defaults []platformmodule.TimedTask, surfaces ...platformmodule.Surface) error {
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
rootParentID := uint(0)
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}
+2 -2
View File
@@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
platformmodule "kra/pkg/module"
)
@@ -20,7 +20,7 @@ func TestSeedSystemCreatesInitialDataAndModuleSurface(t *testing.T) {
Menus: []platformmodule.Menu{{Name: "orders", Path: "orders", ParentName: "extensions", Component: "view/orders.vue", Title: "订单", Sort: 6}},
APIs: []platformmodule.API{{Path: "/orders", Method: "GET", Group: "订单", Description: "订单列表"}},
}
input := &biz.DatabaseConfig{AdminPassword: "admin-password", APIs: []*biz.API{{Path: "/healthz", Method: "GET", APIGroup: "系统"}}}
input := &system.DatabaseConfig{AdminPassword: "admin-password", APIs: []*system.API{{Path: "/healthz", Method: "GET", APIGroup: "系统"}}}
if err = SeedSystem(context.Background(), db, input, surface); err != nil {
t.Fatal(err)
}
+16 -16
View File
@@ -2,7 +2,7 @@ package system
import (
"context"
"kra/internal/biz"
"kra/internal/biz/system"
"kra/pkg/database/gormkit"
"kra/pkg/database/pagination"
"time"
@@ -46,14 +46,14 @@ func (taskLogPO) TableName() string { return "sys_timed_task_logs" }
type taskRepo struct{ data Provider }
func NewTaskRepo(data Provider) biz.TaskRepo { return &taskRepo{data: data} }
func taskToPO(v *biz.TimedTask) taskPO {
func NewTaskRepo(data Provider) system.TaskRepo { return &taskRepo{data: data} }
func taskToPO(v *system.TimedTask) taskPO {
return taskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: gormkit.JSON(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: gormkit.JSON(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
}
func taskFromPO(v taskPO) *biz.TimedTask {
return &biz.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
func taskFromPO(v taskPO) *system.TimedTask {
return &system.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
}
func (r *taskRepo) CreateTask(ctx context.Context, v *biz.TimedTask) error {
func (r *taskRepo) CreateTask(ctx context.Context, v *system.TimedTask) error {
po := taskToPO(v)
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@@ -70,26 +70,26 @@ func (r *taskRepo) TaskNameExists(ctx context.Context, name string, excludeID ui
err := db.Count(&count).Error
return count > 0, err
}
func (r *taskRepo) UpdateTask(ctx context.Context, v *biz.TimedTask) error {
func (r *taskRepo) UpdateTask(ctx context.Context, v *system.TimedTask) error {
po := taskToPO(v)
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error
}
func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error {
return r.data.DB().WithContext(ctx).Delete(&taskPO{}, id).Error
}
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*biz.TimedTask, error) {
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*system.TimedTask, error) {
var po taskPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return taskFromPO(po), nil
}
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]*biz.TimedTask, int64, error) {
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *system.TimedTask) ([]*system.TimedTask, int64, error) {
// During first-install the data layer intentionally serves a bootstrap
// database without system tables. The scheduler starts before /init/initdb
// and should remain idle instead of logging a missing-table SQL error.
if !r.data.DatabaseReady() {
return []*biz.TimedTask{}, 0, nil
return []*system.TimedTask{}, 0, nil
}
db := r.data.DB().WithContext(ctx).Model(&taskPO{})
if q != nil {
@@ -111,7 +111,7 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTask, 0, len(pos))
out := make([]*system.TimedTask, 0, len(pos))
for _, po := range pos {
out = append(out, taskFromPO(po))
}
@@ -120,13 +120,13 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error {
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", id).Update("enabled", enabled).Error
}
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *biz.TimedTaskLog) error {
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *system.TimedTaskLog) error {
return r.data.DB().WithContext(ctx).Create(&taskLogPO{TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}).Error
}
func taskLogFromPO(v taskLogPO) *biz.TimedTaskLog {
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
func taskLogFromPO(v taskLogPO) *system.TimedTaskLog {
return &system.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
}
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*biz.TimedTaskLog, int64, error) {
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*system.TimedTaskLog, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&taskLogPO{})
if taskID != 0 {
db = db.Where("task_id = ?", taskID)
@@ -142,7 +142,7 @@ func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTaskLog, 0, len(pos))
out := make([]*system.TimedTaskLog, 0, len(pos))
for _, po := range pos {
out = append(out, taskLogFromPO(po))
}
@@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@@ -31,7 +31,7 @@ func TestUserAuthorityWritesAreAtomic(t *testing.T) {
t.Fatal(err)
}
repo := &userRepo{data: data}
user := &biz.User{Username: "atomic", Password: "hash", NickName: "before", AuthorityID: 888, Enable: 1}
user := &system.User{Username: "atomic", Password: "hash", NickName: "before", AuthorityID: 888, Enable: 1}
created, err := repo.CreateUserWithAuthorities(ctx, user, []uint{888, 999})
if err != nil {
t.Fatal(err)
@@ -61,7 +61,7 @@ func TestUserAuthorityWritesAreAtomic(t *testing.T) {
t.Fatalf("missing-authority link count = %d, err = %v", missingAuthorityLinks, err)
}
withNewAuthority := &biz.User{Username: "with-new-authority", Password: "hash", AuthorityID: 888, Enable: 1}
withNewAuthority := &system.User{Username: "with-new-authority", Password: "hash", AuthorityID: 888, Enable: 1}
createdWithNewAuthority, err := repo.CreateUserWithAuthorities(ctx, withNewAuthority, []uint{888, 123456})
if err != nil {
t.Fatal(err)
@@ -116,7 +116,7 @@ func TestUserRegistrationDoesNotAddPrimaryAuthorityToRequestedAssociations(t *te
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{{AuthorityID: 888, AuthorityName: "primary"}, {AuthorityID: 999, AuthorityName: "requested"}}).Error; err != nil {
t.Fatal(err)
}
created, err := (&userRepo{data: data}).CreateUserWithAuthorities(ctx, &biz.User{Username: "association-shape", Password: "hash", AuthorityID: 888, Enable: 1}, []uint{999})
created, err := (&userRepo{data: data}).CreateUserWithAuthorities(ctx, &system.User{Username: "association-shape", Password: "hash", AuthorityID: 888, Enable: 1}, []uint{999})
if err != nil {
t.Fatal(err)
}
@@ -173,8 +173,8 @@ func TestDictionaryImportKeepsHierarchyInOneTransaction(t *testing.T) {
repo := &dictionaryRepo{data: data}
parentID := uint(10)
active := true
dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: &active}
details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: &active}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: &active}}
dictionary := &system.Dictionary{Name: "status", Type: "status", Status: &active}
details := []*system.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: &active}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: &active}}
if err := repo.ImportDictionary(ctx, dictionary, details); err != nil {
t.Fatal(err)
}
+39 -39
View File
@@ -4,11 +4,11 @@ import (
"context"
"encoding/json"
"errors"
"kra/internal/biz/system"
"strconv"
"strings"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"github.com/google/uuid"
@@ -16,40 +16,40 @@ import (
"gorm.io/gorm/clause"
)
func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) {
func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*system.User, error) {
var po userPO
if err := r.data.DB().WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUserNotFound
return nil, system.ErrUserNotFound
}
return nil, err
}
return r.loadUser(ctx, &po)
}
func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) {
func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*system.User, error) {
var po userPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUserNotFound
return nil, system.ErrUserNotFound
}
return nil, err
}
return r.loadUser(ctx, &po)
}
func (r *userRepo) FindUserByUUID(ctx context.Context, value string) (*biz.User, error) {
func (r *userRepo) FindUserByUUID(ctx context.Context, value string) (*system.User, error) {
var po userPO
if err := r.data.DB().WithContext(ctx).Where("uuid = ?", value).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUserNotFound
return nil, system.ErrUserNotFound
}
return nil, err
}
return r.loadUser(ctx, &po)
}
func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error) {
func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*system.User, error) {
var authority authorityPO
if err := r.data.DB().WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
@@ -58,7 +58,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error)
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 {
return nil, err
}
authorities := make([]biz.Authority, 0, len(authorityPOs))
authorities := make([]system.Authority, 0, len(authorityPOs))
for _, item := range authorityPOs {
authorities = append(authorities, toBizAuthority(item))
}
@@ -70,12 +70,12 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error)
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 {
return nil, err
}
departments := make([]biz.Department, 0, len(departmentPOs))
departments := make([]system.Department, 0, len(departmentPOs))
for _, item := range departmentPOs {
value := deptFromPO(item)
departments = append(departments, *value)
}
var primary *biz.Department
var primary *system.Department
if po.DeptID != 0 {
var primaryPO departmentPO
if err := r.data.DB().WithContext(ctx).First(&primaryPO, po.DeptID).Error; err == nil {
@@ -88,33 +88,33 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error)
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 {
return nil, err
}
positions := make([]biz.Position, 0, len(positionPOs))
positions := make([]system.Position, 0, len(positionPOs))
for _, item := range positionPOs {
positions = append(positions, *posFromPO(item))
}
return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
}
func toBizAuthority(po authorityPO) biz.Authority {
func toBizAuthority(po authorityPO) system.Authority {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return biz.Authority{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
return system.Authority{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
}
func baseBizUser(po *userPO) *biz.User {
func baseBizUser(po *userPO) *system.User {
setting := map[string]any(nil)
if po.OriginSetting != "" {
_ = json.Unmarshal([]byte(po.OriginSetting), &setting)
}
return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}
}
func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) {
func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*system.User, error) {
if len(pos) == 0 {
return []*biz.User{}, nil
return []*system.User{}, nil
}
db := r.data.DB().WithContext(ctx)
userIDs := make([]uint, 0, len(pos))
@@ -186,18 +186,18 @@ func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*biz.User, er
positionByID[item.ID] = item
}
users := make([]*biz.User, 0, len(pos))
users := make([]*system.User, 0, len(pos))
for i := range pos {
po := &pos[i]
user := baseBizUser(po)
user.Authority = toBizAuthority(authorityByID[po.AuthorityID])
user.Authorities = make([]biz.Authority, 0, len(authorityByUser[po.ID]))
user.Authorities = make([]system.Authority, 0, len(authorityByUser[po.ID]))
for _, id := range authorityByUser[po.ID] {
if item, ok := authorityByID[id]; ok {
user.Authorities = append(user.Authorities, toBizAuthority(item))
}
}
user.Departments = make([]biz.Department, 0, len(departmentByUser[po.ID]))
user.Departments = make([]system.Department, 0, len(departmentByUser[po.ID]))
for _, id := range departmentByUser[po.ID] {
if item, ok := departmentByID[id]; ok {
user.Departments = append(user.Departments, *deptFromPO(item))
@@ -206,7 +206,7 @@ func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*biz.User, er
if primary, ok := departmentByID[po.DeptID]; ok {
user.Department = deptFromPO(primary)
}
user.Positions = make([]biz.Position, 0, len(positionByUser[po.ID]))
user.Positions = make([]system.Position, 0, len(positionByUser[po.ID]))
for _, id := range positionByUser[po.ID] {
if item, ok := positionByID[id]; ok {
user.Positions = append(user.Positions, *posFromPO(item))
@@ -226,7 +226,7 @@ func (r *userRepo) HasAuthorityMenu(ctx context.Context, authorityID uint, name
return count > 0, err
}
func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
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").
Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
@@ -235,13 +235,13 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
if err != nil {
return nil, err
}
byID := make(map[uint]*biz.Menu, len(pos))
byID := make(map[uint]*system.Menu, len(pos))
menuIDs := make([]uint, 0, len(pos))
for _, po := range pos {
menu := menuFromPO(po)
menu.MenuID = po.ID
menu.Children = nil
menu.Parameters = []*biz.MenuParameter{}
menu.Parameters = []*system.MenuParameter{}
menu.Buttons = nil
byID[po.ID] = menu
menuIDs = append(menuIDs, po.ID)
@@ -266,7 +266,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
menu.Buttons = append(menu.Buttons, value)
}
}
var roots []*biz.Menu
var roots []*system.Menu
for _, po := range pos {
menu := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
@@ -278,7 +278,7 @@ func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*b
return roots, nil
}
func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) {
func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *system.UserListFilter) ([]*system.User, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&userPO{})
order := "id desc"
if filter != nil {
@@ -314,11 +314,11 @@ func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *bi
return users, total, err
}
func (r *userRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) {
func (r *userRepo) CreateUser(ctx context.Context, user *system.User) (*system.User, error) {
return r.CreateUserWithAuthorities(ctx, user, nil)
}
func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) {
func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *system.User, authorityIDs []uint) (*system.User, error) {
requestedAuthorities := append([]uint{user.AuthorityID}, authorityIDs...)
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDsAuth(ctx, requestedAuthorities); err != nil {
return nil, err
@@ -337,7 +337,7 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User
}
now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
var createdAuthorities []biz.Authority
var createdAuthorities []system.Authority
if err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// The compatible registration rejects an existing non-deleted username before creating the
// account. The username column is intentionally not unique for legacy
@@ -381,14 +381,14 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User
return created, nil
}
func (r *userRepo) UpdateUser(ctx context.Context, user *biz.User) error {
func (r *userRepo) UpdateUser(ctx context.Context, user *system.User) error {
if err := r.checkUserUpdateAuth(ctx, user); err != nil {
return err
}
return r.updateUser(r.data.DB().WithContext(ctx), user)
}
func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *biz.User) error {
func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *system.User) error {
access := &authorityAccessRepo{data: r.data}
if err := access.checkUserIDAuth(ctx, user.ID, false); err != nil {
return err
@@ -399,7 +399,7 @@ func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *biz.User) erro
return nil
}
func (r *userRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
func (r *userRepo) UpdateSelfUser(ctx context.Context, user *system.User) error {
updates := make(map[string]any)
if user.NickName != "" {
updates["nick_name"] = user.NickName
@@ -422,7 +422,7 @@ func (r *userRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
}
func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error {
func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *system.User) error {
if user == nil || len(user.Departments) == 0 {
return nil
}
@@ -463,7 +463,7 @@ func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User)
return nil
}
func (r *userRepo) updateUser(tx *gorm.DB, user *biz.User) error {
func (r *userRepo) updateUser(tx *gorm.DB, user *system.User) error {
updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable}
if user.AuthorityID != 0 {
updates["authority_id"] = user.AuthorityID
@@ -471,7 +471,7 @@ func (r *userRepo) updateUser(tx *gorm.DB, user *biz.User) error {
return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
}
func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error {
func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *system.User, authorityIDs []uint) error {
access := &authorityAccessRepo{data: r.data}
if err := r.checkUserUpdateAuth(ctx, user); err != nil {
return err
@@ -520,12 +520,12 @@ func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string,
}
return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error
}
func (r *userRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
func (r *userRepo) ListAuthorities(ctx context.Context) ([]*system.Authority, error) {
var pos []authorityPO
if err := r.data.DB().WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
return nil, err
}
result := make([]*biz.Authority, 0, len(pos))
result := make([]*system.Authority, 0, len(pos))
for _, po := range pos {
a := toBizAuthority(po)
result = append(result, &a)
+9 -10
View File
@@ -2,9 +2,8 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
)
func seedStrictUserTree(t *testing.T, data *Data) (actorID, childID, siblingID uint, users []userPO) {
@@ -35,10 +34,10 @@ func seedStrictUserTree(t *testing.T, data *Data) (actorID, childID, siblingID u
func TestStrictUserMutationsRejectOutsideTargetAndAllowSelfPassword(t *testing.T) {
data := newTransactionTestData(t)
actorID, _, _, users := seedStrictUserTree(t, data)
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: users[0].ID, AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: users[0].ID, AuthorityID: actorID})
repo := &userRepo{data: data}
if err := repo.UpdateUser(ctx, &biz.User{ID: users[2].ID, NickName: "changed"}); err == nil {
if err := repo.UpdateUser(ctx, &system.User{ID: users[2].ID, NickName: "changed"}); err == nil {
t.Fatal("UpdateUser() accepted an out-of-scope target")
}
if err := repo.DeleteUser(ctx, users[2].ID); err == nil {
@@ -55,13 +54,13 @@ func TestStrictUserMutationsRejectOutsideTargetAndAllowSelfPassword(t *testing.T
func TestStrictUserCreationAndRoleAssignmentRequireManagedAuthorities(t *testing.T) {
data := newTransactionTestData(t)
actorID, childID, siblingID, users := seedStrictUserTree(t, data)
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: users[0].ID, AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: users[0].ID, AuthorityID: actorID})
repo := &userRepo{data: data}
if _, err := repo.CreateUserWithAuthorities(ctx, &biz.User{Username: "outside-create", Password: "hash", AuthorityID: siblingID, Enable: 1}, nil); err == nil {
if _, err := repo.CreateUserWithAuthorities(ctx, &system.User{Username: "outside-create", Password: "hash", AuthorityID: siblingID, Enable: 1}, nil); err == nil {
t.Fatal("CreateUserWithAuthorities() accepted an out-of-scope primary authority")
}
if _, err := repo.CreateUserWithAuthorities(ctx, &biz.User{Username: "managed-create", Password: "hash", AuthorityID: childID, Enable: 1}, []uint{childID}); err != nil {
if _, err := repo.CreateUserWithAuthorities(ctx, &system.User{Username: "managed-create", Password: "hash", AuthorityID: childID, Enable: 1}, []uint{childID}); err != nil {
t.Fatalf("CreateUserWithAuthorities() rejected managed authorities: %v", err)
}
if err := repo.SetUserAuthorities(ctx, users[1].ID, []uint{siblingID}); err == nil {
@@ -75,13 +74,13 @@ func TestStrictUserCreationAndRoleAssignmentRequireManagedAuthorities(t *testing
func TestStrictUserUpdateRejectsPrimaryAuthorityOutsideManagedTree(t *testing.T) {
data := newTransactionTestData(t)
actorID, childID, siblingID, users := seedStrictUserTree(t, data)
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: users[0].ID, AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: users[0].ID, AuthorityID: actorID})
repo := &userRepo{data: data}
if err := repo.UpdateUser(ctx, &biz.User{ID: users[1].ID, NickName: "changed", AuthorityID: siblingID}); err == nil {
if err := repo.UpdateUser(ctx, &system.User{ID: users[1].ID, NickName: "changed", AuthorityID: siblingID}); err == nil {
t.Fatal("UpdateUser() accepted a primary authority outside the managed tree")
}
if err := repo.UpdateUserWithAuthorities(ctx, &biz.User{ID: users[1].ID, NickName: "changed", AuthorityID: siblingID}, []uint{childID}); err == nil {
if err := repo.UpdateUserWithAuthorities(ctx, &system.User{ID: users[1].ID, NickName: "changed", AuthorityID: siblingID}, []uint{childID}); err == nil {
t.Fatal("UpdateUserWithAuthorities() accepted a primary authority outside the managed tree")
}
+33 -32
View File
@@ -2,12 +2,13 @@ package system
import (
"context"
"kra/internal/biz/system"
"sort"
"time"
"gorm.io/gorm"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
)
type versionPO struct {
@@ -25,11 +26,11 @@ func (versionPO) TableName() string { return "sys_versions" }
type versionRepo struct{ data Provider }
func NewVersionRepo(data Provider) biz.VersionRepo { return &versionRepo{data: data} }
func versionFromPO(v versionPO) *biz.Version {
return &biz.Version{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data}
func NewVersionRepo(data Provider) system.VersionRepo { return &versionRepo{data: data} }
func versionFromPO(v versionPO) *system.Version {
return &system.Version{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data}
}
func (r *versionRepo) CreateVersion(ctx context.Context, v *biz.Version) error {
func (r *versionRepo) CreateVersion(ctx context.Context, v *system.Version) error {
po := versionPO{Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@@ -40,14 +41,14 @@ func (r *versionRepo) CreateVersion(ctx context.Context, v *biz.Version) error {
func (r *versionRepo) DeleteVersions(ctx context.Context, ids []uint) error {
return r.data.DB().WithContext(ctx).Where("id IN ?", ids).Delete(&versionPO{}).Error
}
func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, error) {
func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*system.Version, error) {
var po versionPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return versionFromPO(po), nil
}
func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string, createdAtRange []*time.Time) ([]*biz.Version, int64, error) {
func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string, createdAtRange []*time.Time) ([]*system.Version, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&versionPO{})
if len(createdAtRange) == 2 && createdAtRange[0] != nil && createdAtRange[1] != nil {
db = db.Where("created_at BETWEEN ? AND ?", createdAtRange[0], createdAtRange[1])
@@ -66,21 +67,21 @@ func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, co
if err := pagination.Apply(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Version, 0, len(pos))
out := make([]*system.Version, 0, len(pos))
for _, po := range pos {
out = append(out, versionFromPO(po))
}
return out, total, nil
}
func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, dictIDs []uint) (*biz.VersionBundle, error) {
bundle := &biz.VersionBundle{}
func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, dictIDs []uint) (*system.VersionBundle, error) {
bundle := &system.VersionBundle{}
var menus []menuPO
if len(menuIDs) > 0 {
if err := r.data.DB().WithContext(ctx).Where("id IN ?", menuIDs).Order("sort,id").Find(&menus).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
return nil, &system.VersionStageError{Stage: system.VersionStageMenus, Err: err}
}
}
byID := map[uint]*biz.Menu{}
byID := map[uint]*system.Menu{}
for _, po := range menus {
v := menuFromPO(po)
byID[po.ID] = v
@@ -88,20 +89,20 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
if len(menuIDs) > 0 {
var buttons []menuButtonPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&buttons).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
return nil, &system.VersionStageError{Stage: system.VersionStageMenus, Err: err}
}
for _, button := range buttons {
if menu := byID[button.MenuID]; menu != nil {
menu.Buttons = append(menu.Buttons, &biz.MenuButton{Name: button.Name, Description: button.Description})
menu.Buttons = append(menu.Buttons, &system.MenuButton{Name: button.Name, Description: button.Description})
}
}
var parameters []menuParameterPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(&parameters).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
return nil, &system.VersionStageError{Stage: system.VersionStageMenus, Err: err}
}
for _, parameter := range parameters {
if menu := byID[parameter.MenuID]; menu != nil {
menu.Parameters = append(menu.Parameters, &biz.MenuParameter{Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
menu.Parameters = append(menu.Parameters, &system.MenuParameter{Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
}
}
}
@@ -113,16 +114,16 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
bundle.Menus = append(bundle.Menus, v)
}
}
var sortMenus func([]*biz.Menu)
sortMenus = func(items []*biz.Menu) {
var sortMenus func([]*system.Menu)
sortMenus = func(items []*system.Menu) {
sort.Slice(items, func(i, j int) bool { return items[i].Sort < items[j].Sort })
for _, item := range items {
sortMenus(item.Children)
}
}
sortMenus(bundle.Menus)
var cleanMenu func(*biz.Menu)
cleanMenu = func(menu *biz.Menu) {
var cleanMenu func(*system.Menu)
cleanMenu = func(menu *system.Menu) {
menu.ID = 0
menu.ParentID = 0
for _, button := range menu.Buttons {
@@ -144,7 +145,7 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
var apis []apiPO
if len(apiIDs) > 0 {
if err := r.data.DB().WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err}
return nil, &system.VersionStageError{Stage: system.VersionStageAPIs, Err: err}
}
}
for _, po := range apis {
@@ -155,16 +156,16 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
var dictionaries []dictionaryPO
if len(dictIDs) > 0 {
if err := r.data.DB().WithContext(ctx).Where("id IN ?", dictIDs).Find(&dictionaries).Error; err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
return nil, &system.VersionStageError{Stage: system.VersionStageDictionaries, Err: err}
}
}
dictionariesRepo := &dictionaryRepo{data: r.data}
for _, po := range dictionaries {
v := dictionaryFromPO(po)
v.ID = 0
details, _, err := dictionariesRepo.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: po.ID})
details, _, err := dictionariesRepo.ListDictionaryDetails(ctx, 0, 0, system.DictionaryDetailFilter{DictionaryID: po.ID})
if err != nil {
return nil, &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
return nil, &system.VersionStageError{Stage: system.VersionStageDictionaries, Err: err}
}
for _, d := range details {
d.ID = 0
@@ -178,27 +179,27 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d
}
return bundle, nil
}
func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBundle) error {
func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *system.VersionBundle) error {
db := r.data.DB().WithContext(ctx)
if len(b.Menus) > 0 {
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionMenus(tx, b.Menus, 0) }); err != nil {
return &biz.VersionStageError{Stage: biz.VersionStageMenus, Err: err}
return &system.VersionStageError{Stage: system.VersionStageMenus, Err: err}
}
}
if len(b.APIs) > 0 {
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionAPIs(tx, b.APIs) }); err != nil {
return &biz.VersionStageError{Stage: biz.VersionStageAPIs, Err: err}
return &system.VersionStageError{Stage: system.VersionStageAPIs, Err: err}
}
}
if len(b.Dictionaries) > 0 {
if err := db.Transaction(func(tx *gorm.DB) error { return importVersionDictionaries(tx, b.Dictionaries) }); err != nil {
return &biz.VersionStageError{Stage: biz.VersionStageDictionaries, Err: err}
return &system.VersionStageError{Stage: system.VersionStageDictionaries, Err: err}
}
}
return nil
}
func importVersionMenus(tx *gorm.DB, items []*biz.Menu, parentID uint) error {
func importVersionMenus(tx *gorm.DB, items []*system.Menu, parentID uint) error {
for _, item := range items {
var existing menuPO
if err := tx.Where("name = ? AND path = ?", item.Name, item.Path).First(&existing).Error; err == nil {
@@ -230,7 +231,7 @@ func importVersionMenus(tx *gorm.DB, items []*biz.Menu, parentID uint) error {
return nil
}
func importVersionAPIs(tx *gorm.DB, items []*biz.API) error {
func importVersionAPIs(tx *gorm.DB, items []*system.API) error {
for _, item := range items {
var existing apiPO
if err := tx.Where("path = ? AND method = ?", item.Path, item.Method).First(&existing).Error; err == nil {
@@ -243,7 +244,7 @@ func importVersionAPIs(tx *gorm.DB, items []*biz.API) error {
return nil
}
func importVersionDictionaries(tx *gorm.DB, items []*biz.Dictionary) error {
func importVersionDictionaries(tx *gorm.DB, items []*system.Dictionary) error {
for _, item := range items {
var existing dictionaryPO
if err := tx.Where("type = ?", item.Type).First(&existing).Error; err == nil {