Files
lijiaoqiao/supply-api/internal/domain/invariants.go
Your Name ed0961d486 fix(supply-api): 修复编译错误和测试问题
- 添加 ErrNotFound 和 ErrConcurrencyConflict 错误定义
- 修复 pgx.NullTime 替换为 *time.Time
- 修复 db.go 事务类型 (pgx.Tx vs pgxpool.Tx)
- 移除未使用的导入和变量
- 修复 NewSupplyAPI 调用参数
- 修复中间件链路 handler 类型问题
- 修复适配器类型引用 (storage.InMemoryAccountStore 等)
- 所有测试通过

Test: go test ./...
2026-04-01 13:03:44 +08:00

213 lines
6.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package domain
import (
"context"
"errors"
"fmt"
)
// 领域不变量错误
var (
// INV-ACC-001: active账号不可删除
ErrAccountCannotDeleteActive = errors.New("SUP_ACC_4092: cannot delete active accounts")
// INV-ACC-002: disabled账号仅管理员可恢复
ErrAccountDisabledRequiresAdmin = errors.New("SUP_ACC_4031: disabled account requires admin to restore")
// INV-PKG-001: sold_out只能系统迁移
ErrPackageSoldOutSystemOnly = errors.New("SUP_PKG_4092: sold_out status can only be changed by system")
// INV-PKG-002: expired套餐不可直接恢复
ErrPackageExpiredCannotRestore = errors.New("SUP_PKG_4093: expired package cannot be directly restored")
// INV-PKG-003: 售价不得低于保护价
ErrPriceBelowProtection = errors.New("SUP_PKG_4001: price cannot be below protected price")
// INV-SET-001: processing/completed不可撤销
ErrSettlementCannotCancel = errors.New("SUP_SET_4092: cannot cancel processing or completed settlements")
// INV-SET-002: 提现金额不得超过可提现余额
ErrWithdrawExceedsBalance = errors.New("SUP_SET_4001: withdraw amount exceeds available balance")
// INV-SET-003: 结算单金额与余额流水必须平衡
ErrSettlementBalanceMismatch = errors.New("SUP_SET_5002: settlement amount does not match balance ledger")
)
// InvariantChecker 领域不变量检查器
type InvariantChecker struct {
accountStore AccountStore
packageStore PackageStore
settlementStore SettlementStore
}
// NewInvariantChecker 创建不变量检查器
func NewInvariantChecker(
accountStore AccountStore,
packageStore PackageStore,
settlementStore SettlementStore,
) *InvariantChecker {
return &InvariantChecker{
accountStore: accountStore,
packageStore: packageStore,
settlementStore: settlementStore,
}
}
// CheckAccountDelete 检查账号删除不变量
func (c *InvariantChecker) CheckAccountDelete(ctx context.Context, accountID, supplierID int64) error {
account, err := c.accountStore.GetByID(ctx, supplierID, accountID)
if err != nil {
return err
}
// INV-ACC-001: active账号不可删除
if account.Status == AccountStatusActive {
return ErrAccountCannotDeleteActive
}
return nil
}
// CheckAccountActivate 检查账号激活不变量
func (c *InvariantChecker) CheckAccountActivate(ctx context.Context, accountID, supplierID int64) error {
account, err := c.accountStore.GetByID(ctx, supplierID, accountID)
if err != nil {
return err
}
// INV-ACC-002: disabled账号仅管理员可恢复简化处理实际需要检查角色
if account.Status == AccountStatusDisabled {
return ErrAccountDisabledRequiresAdmin
}
return nil
}
// CheckPackagePublish 检查套餐发布不变量
func (c *InvariantChecker) CheckPackagePublish(ctx context.Context, packageID, supplierID int64) error {
pkg, err := c.packageStore.GetByID(ctx, supplierID, packageID)
if err != nil {
return err
}
// INV-PKG-002: expired套餐不可直接恢复
if pkg.Status == PackageStatusExpired {
return ErrPackageExpiredCannotRestore
}
return nil
}
// CheckPackagePrice 检查套餐价格不变量
func (c *InvariantChecker) CheckPackagePrice(ctx context.Context, pkg *Package, newPricePer1MInput, newPricePer1MOutput float64) error {
// INV-PKG-003: 售价不得低于保护价(这里简化处理,实际需要查询保护价配置)
minPrice := 0.01
if newPricePer1MInput > 0 && newPricePer1MInput < minPrice {
return fmt.Errorf("%w: input price %.6f is below minimum %.6f",
ErrPriceBelowProtection, newPricePer1MInput, minPrice)
}
if newPricePer1MOutput > 0 && newPricePer1MOutput < minPrice {
return fmt.Errorf("%w: output price %.6f is below minimum %.6f",
ErrPriceBelowProtection, newPricePer1MOutput, minPrice)
}
return nil
}
// CheckSettlementCancel 检查结算撤销不变量
func (c *InvariantChecker) CheckSettlementCancel(ctx context.Context, settlementID, supplierID int64) error {
settlement, err := c.settlementStore.GetByID(ctx, supplierID, settlementID)
if err != nil {
return err
}
// INV-SET-001: processing/completed不可撤销
if settlement.Status == SettlementStatusProcessing || settlement.Status == SettlementStatusCompleted {
return ErrSettlementCannotCancel
}
return nil
}
// CheckWithdrawBalance 检查提现余额不变量
func (c *InvariantChecker) CheckWithdrawBalance(ctx context.Context, supplierID int64, amount float64) error {
balance, err := c.settlementStore.GetWithdrawableBalance(ctx, supplierID)
if err != nil {
return err
}
// INV-SET-002: 提现金额不得超过可提现余额
if amount > balance {
return fmt.Errorf("%w: requested %.2f but available %.2f",
ErrWithdrawExceedsBalance, amount, balance)
}
return nil
}
// InvariantViolation 领域不变量违反事件
type InvariantViolation struct {
RuleCode string
ObjectType string
ObjectID int64
Message string
OccurredAt string
}
// EmitInvariantViolation 发射不变量违反事件
func EmitInvariantViolation(ruleCode, objectType string, objectID int64, err error) *InvariantViolation {
return &InvariantViolation{
RuleCode: ruleCode,
ObjectType: objectType,
ObjectID: objectID,
Message: err.Error(),
OccurredAt: "now", // 实际应使用时间戳
}
}
// ValidateStateTransition 验证状态转换是否合法
func ValidateStateTransition(from, to AccountStatus) bool {
validTransitions := map[AccountStatus][]AccountStatus{
AccountStatusPending: {AccountStatusActive, AccountStatusDisabled},
AccountStatusActive: {AccountStatusSuspended, AccountStatusDisabled},
AccountStatusSuspended: {AccountStatusActive, AccountStatusDisabled},
AccountStatusDisabled: {AccountStatusActive}, // 需要管理员权限
}
allowed, ok := validTransitions[from]
if !ok {
return false
}
for _, status := range allowed {
if status == to {
return true
}
}
return false
}
// ValidatePackageStateTransition 验证套餐状态转换
func ValidatePackageStateTransition(from, to PackageStatus) bool {
validTransitions := map[PackageStatus][]PackageStatus{
PackageStatusDraft: {PackageStatusActive},
PackageStatusActive: {PackageStatusPaused, PackageStatusSoldOut, PackageStatusExpired},
PackageStatusPaused: {PackageStatusActive, PackageStatusExpired},
PackageStatusSoldOut: {}, // 只能由系统迁移
PackageStatusExpired: {}, // 不能直接恢复,需要通过克隆
}
allowed, ok := validTransitions[from]
if !ok {
return false
}
for _, status := range allowed {
if status == to {
return true
}
}
return false
}