feat: add operator and business reference fields to tenant ledgers
- Added `operator_user_id`, `biz_ref_type`, and `biz_ref_id` fields to the TenantLedger model for enhanced auditing and traceability. - Updated the tenant ledgers query generation to include new fields. - Introduced new API endpoint for retrieving tenant ledger records with filtering options based on the new fields. - Enhanced Swagger documentation to reflect the new endpoint and its parameters. - Created DTOs for admin ledger filtering and item representation. - Implemented the admin ledger retrieval logic in the tenant service. - Added database migration scripts to introduce new fields and indexes for efficient querying.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE tenant_ledgers
|
||||
ADD COLUMN IF NOT EXISTS operator_user_id bigint,
|
||||
ADD COLUMN IF NOT EXISTS biz_ref_type varchar(32),
|
||||
ADD COLUMN IF NOT EXISTS biz_ref_id bigint;
|
||||
|
||||
-- tenant_ledgers.operator_user_id:操作者(谁触发该流水)
|
||||
-- 用途:用于审计与风控追溯(例如后台代充值/代退款/调账等)。
|
||||
COMMENT ON COLUMN tenant_ledgers.operator_user_id IS '操作者用户ID:谁触发该流水(admin/buyer/system);用于审计与追责;可为空(历史数据或无法识别时)';
|
||||
|
||||
-- tenant_ledgers.biz_ref_type/biz_ref_id:业务引用(幂等与追溯)
|
||||
-- 用途:在 idempotency_key 之外提供结构化引用(例如 order/refund/topup 等),便于报表与按业务对象追溯。
|
||||
COMMENT ON COLUMN tenant_ledgers.biz_ref_type IS '业务引用类型:order/refund/topup/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键';
|
||||
COMMENT ON COLUMN tenant_ledgers.biz_ref_id IS '业务引用ID:与 biz_ref_type 配合使用(例如 orders.id);用于对账与审计';
|
||||
|
||||
-- 索引:按操作者检索敏感操作流水(后台审计用)。
|
||||
CREATE INDEX IF NOT EXISTS ix_tenant_ledgers_tenant_operator ON tenant_ledgers(tenant_id, operator_user_id);
|
||||
|
||||
-- 索引:按业务引用快速定位同一业务对象的流水集合。
|
||||
CREATE INDEX IF NOT EXISTS ix_tenant_ledgers_tenant_biz_ref ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id);
|
||||
|
||||
-- 结构化幂等(可选):同一业务引用在同一流水类型下只能出现一条。
|
||||
-- 说明:biz_ref_* 允许为空;仅当两者都非空时才参与唯一性约束。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type
|
||||
ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id, type)
|
||||
WHERE biz_ref_type IS NOT NULL AND biz_ref_id IS NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP INDEX IF EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type;
|
||||
DROP INDEX IF EXISTS ix_tenant_ledgers_tenant_biz_ref;
|
||||
DROP INDEX IF EXISTS ix_tenant_ledgers_tenant_operator;
|
||||
|
||||
ALTER TABLE tenant_ledgers
|
||||
DROP COLUMN IF EXISTS biz_ref_id,
|
||||
DROP COLUMN IF EXISTS biz_ref_type,
|
||||
DROP COLUMN IF EXISTS operator_user_id;
|
||||
-- +goose StatementEnd
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- 修正:biz_ref_type/biz_ref_id 在 Go 模型侧为 string/int64(非指针),空值会写入 ''/0,
|
||||
-- 若唯一索引仅判断 NOT NULL,会导致大量流水写入冲突。
|
||||
-- 约束策略:仅当 biz_ref_type 非空 且 biz_ref_id > 0 时才参与唯一性约束。
|
||||
DROP INDEX IF EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type
|
||||
ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id, type)
|
||||
WHERE biz_ref_type IS NOT NULL AND biz_ref_type <> '' AND biz_ref_id IS NOT NULL AND biz_ref_id <> 0;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP INDEX IF EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type;
|
||||
|
||||
-- Down 回滚为“仅判断 NOT NULL”的版本(不建议在线上使用该版本)。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type
|
||||
ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id, type)
|
||||
WHERE biz_ref_type IS NOT NULL AND biz_ref_id IS NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
@@ -28,8 +28,8 @@ type OrderItem struct {
|
||||
Snapshot types.JSONType[fields.OrderItemsSnapshot] `gorm:"column:snapshot;type:jsonb;not null;default:{};comment:内容快照:JSON;建议包含 title/price/discount 等,用于历史展示与审计" json:"snapshot"` // 内容快照:JSON;建议包含 title/price/discount 等,用于历史展示与审计
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now();comment:创建时间:默认 now()" json:"created_at"` // 创建时间:默认 now()
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now();comment:更新时间:默认 now()" json:"updated_at"` // 更新时间:默认 now()
|
||||
Order *Order `gorm:"foreignKey:OrderID;references:ID" json:"order,omitempty"`
|
||||
Content *Content `gorm:"foreignKey:ContentID;references:ID" json:"content,omitempty"`
|
||||
Order *Order `gorm:"foreignKey:OrderID;references:ID" json:"order,omitempty"`
|
||||
}
|
||||
|
||||
// Quick operations without importing query package
|
||||
|
||||
@@ -35,18 +35,18 @@ func newOrderItem(db *gorm.DB, opts ...gen.DOOption) orderItemQuery {
|
||||
_orderItemQuery.Snapshot = field.NewJSONB(tableName, "snapshot")
|
||||
_orderItemQuery.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_orderItemQuery.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_orderItemQuery.Order = orderItemQueryBelongsToOrder{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("Order", "Order"),
|
||||
}
|
||||
|
||||
_orderItemQuery.Content = orderItemQueryBelongsToContent{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("Content", "Content"),
|
||||
}
|
||||
|
||||
_orderItemQuery.Order = orderItemQueryBelongsToOrder{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("Order", "Order"),
|
||||
}
|
||||
|
||||
_orderItemQuery.fillFieldMap()
|
||||
|
||||
return _orderItemQuery
|
||||
@@ -66,9 +66,9 @@ type orderItemQuery struct {
|
||||
Snapshot field.JSONB // 内容快照:JSON;建议包含 title/price/discount 等,用于历史展示与审计
|
||||
CreatedAt field.Time // 创建时间:默认 now()
|
||||
UpdatedAt field.Time // 更新时间:默认 now()
|
||||
Order orderItemQueryBelongsToOrder
|
||||
Content orderItemQueryBelongsToContent
|
||||
|
||||
Content orderItemQueryBelongsToContent
|
||||
Order orderItemQueryBelongsToOrder
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -143,101 +143,20 @@ func (o *orderItemQuery) fillFieldMap() {
|
||||
|
||||
func (o orderItemQuery) clone(db *gorm.DB) orderItemQuery {
|
||||
o.orderItemQueryDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
o.Order.db = db.Session(&gorm.Session{Initialized: true})
|
||||
o.Order.db.Statement.ConnPool = db.Statement.ConnPool
|
||||
o.Content.db = db.Session(&gorm.Session{Initialized: true})
|
||||
o.Content.db.Statement.ConnPool = db.Statement.ConnPool
|
||||
o.Order.db = db.Session(&gorm.Session{Initialized: true})
|
||||
o.Order.db.Statement.ConnPool = db.Statement.ConnPool
|
||||
return o
|
||||
}
|
||||
|
||||
func (o orderItemQuery) replaceDB(db *gorm.DB) orderItemQuery {
|
||||
o.orderItemQueryDo.ReplaceDB(db)
|
||||
o.Order.db = db.Session(&gorm.Session{})
|
||||
o.Content.db = db.Session(&gorm.Session{})
|
||||
o.Order.db = db.Session(&gorm.Session{})
|
||||
return o
|
||||
}
|
||||
|
||||
type orderItemQueryBelongsToOrder struct {
|
||||
db *gorm.DB
|
||||
|
||||
field.RelationField
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Where(conds ...field.Expr) *orderItemQueryBelongsToOrder {
|
||||
if len(conds) == 0 {
|
||||
return &a
|
||||
}
|
||||
|
||||
exprs := make([]clause.Expression, 0, len(conds))
|
||||
for _, cond := range conds {
|
||||
exprs = append(exprs, cond.BeCond().(clause.Expression))
|
||||
}
|
||||
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) WithContext(ctx context.Context) *orderItemQueryBelongsToOrder {
|
||||
a.db = a.db.WithContext(ctx)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Session(session *gorm.Session) *orderItemQueryBelongsToOrder {
|
||||
a.db = a.db.Session(session)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Model(m *OrderItem) *orderItemQueryBelongsToOrderTx {
|
||||
return &orderItemQueryBelongsToOrderTx{a.db.Model(m).Association(a.Name())}
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Unscoped() *orderItemQueryBelongsToOrder {
|
||||
a.db = a.db.Unscoped()
|
||||
return &a
|
||||
}
|
||||
|
||||
type orderItemQueryBelongsToOrderTx struct{ tx *gorm.Association }
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Find() (result *Order, err error) {
|
||||
return result, a.tx.Find(&result)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Append(values ...*Order) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Append(targetValues...)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Replace(values ...*Order) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Replace(targetValues...)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Delete(values ...*Order) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Delete(targetValues...)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Clear() error {
|
||||
return a.tx.Clear()
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Count() int64 {
|
||||
return a.tx.Count()
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Unscoped() *orderItemQueryBelongsToOrderTx {
|
||||
a.tx = a.tx.Unscoped()
|
||||
return &a
|
||||
}
|
||||
|
||||
type orderItemQueryBelongsToContent struct {
|
||||
db *gorm.DB
|
||||
|
||||
@@ -319,6 +238,87 @@ func (a orderItemQueryBelongsToContentTx) Unscoped() *orderItemQueryBelongsToCon
|
||||
return &a
|
||||
}
|
||||
|
||||
type orderItemQueryBelongsToOrder struct {
|
||||
db *gorm.DB
|
||||
|
||||
field.RelationField
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Where(conds ...field.Expr) *orderItemQueryBelongsToOrder {
|
||||
if len(conds) == 0 {
|
||||
return &a
|
||||
}
|
||||
|
||||
exprs := make([]clause.Expression, 0, len(conds))
|
||||
for _, cond := range conds {
|
||||
exprs = append(exprs, cond.BeCond().(clause.Expression))
|
||||
}
|
||||
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) WithContext(ctx context.Context) *orderItemQueryBelongsToOrder {
|
||||
a.db = a.db.WithContext(ctx)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Session(session *gorm.Session) *orderItemQueryBelongsToOrder {
|
||||
a.db = a.db.Session(session)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Model(m *OrderItem) *orderItemQueryBelongsToOrderTx {
|
||||
return &orderItemQueryBelongsToOrderTx{a.db.Model(m).Association(a.Name())}
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrder) Unscoped() *orderItemQueryBelongsToOrder {
|
||||
a.db = a.db.Unscoped()
|
||||
return &a
|
||||
}
|
||||
|
||||
type orderItemQueryBelongsToOrderTx struct{ tx *gorm.Association }
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Find() (result *Order, err error) {
|
||||
return result, a.tx.Find(&result)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Append(values ...*Order) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Append(targetValues...)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Replace(values ...*Order) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Replace(targetValues...)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Delete(values ...*Order) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Delete(targetValues...)
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Clear() error {
|
||||
return a.tx.Clear()
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Count() int64 {
|
||||
return a.tx.Count()
|
||||
}
|
||||
|
||||
func (a orderItemQueryBelongsToOrderTx) Unscoped() *orderItemQueryBelongsToOrderTx {
|
||||
a.tx = a.tx.Unscoped()
|
||||
return &a
|
||||
}
|
||||
|
||||
type orderItemQueryDo struct{ gen.DO }
|
||||
|
||||
func (o orderItemQueryDo) Debug() *orderItemQueryDo {
|
||||
|
||||
@@ -31,6 +31,9 @@ type TenantLedger struct {
|
||||
Remark string `gorm:"column:remark;type:character varying(255);not null;comment:备注:业务说明/后台操作原因等;用于审计" json:"remark"` // 备注:业务说明/后台操作原因等;用于审计
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now();comment:创建时间:默认 now()" json:"created_at"` // 创建时间:默认 now()
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now();comment:更新时间:默认 now()" json:"updated_at"` // 更新时间:默认 now()
|
||||
OperatorUserID int64 `gorm:"column:operator_user_id;type:bigint;comment:操作者用户ID:谁触发该流水(admin/buyer/system);用于审计与追责;可为空(历史数据或无法识别时)" json:"operator_user_id"` // 操作者用户ID:谁触发该流水(admin/buyer/system);用于审计与追责;可为空(历史数据或无法识别时)
|
||||
BizRefType string `gorm:"column:biz_ref_type;type:character varying(32);comment:业务引用类型:order/refund/topup/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键" json:"biz_ref_type"` // 业务引用类型:order/refund/topup/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键
|
||||
BizRefID int64 `gorm:"column:biz_ref_id;type:bigint;comment:业务引用ID:与 biz_ref_type 配合使用(例如 orders.id);用于对账与审计" json:"biz_ref_id"` // 业务引用ID:与 biz_ref_type 配合使用(例如 orders.id);用于对账与审计
|
||||
Order *Order `gorm:"foreignKey:OrderID;references:ID" json:"order,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ func newTenantLedger(db *gorm.DB, opts ...gen.DOOption) tenantLedgerQuery {
|
||||
_tenantLedgerQuery.Remark = field.NewString(tableName, "remark")
|
||||
_tenantLedgerQuery.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_tenantLedgerQuery.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_tenantLedgerQuery.OperatorUserID = field.NewInt64(tableName, "operator_user_id")
|
||||
_tenantLedgerQuery.BizRefType = field.NewString(tableName, "biz_ref_type")
|
||||
_tenantLedgerQuery.BizRefID = field.NewInt64(tableName, "biz_ref_id")
|
||||
_tenantLedgerQuery.Order = tenantLedgerQueryBelongsToOrder{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
@@ -68,6 +71,9 @@ type tenantLedgerQuery struct {
|
||||
Remark field.String // 备注:业务说明/后台操作原因等;用于审计
|
||||
CreatedAt field.Time // 创建时间:默认 now()
|
||||
UpdatedAt field.Time // 更新时间:默认 now()
|
||||
OperatorUserID field.Int64 // 操作者用户ID:谁触发该流水(admin/buyer/system);用于审计与追责;可为空(历史数据或无法识别时)
|
||||
BizRefType field.String // 业务引用类型:order/refund/topup/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键
|
||||
BizRefID field.Int64 // 业务引用ID:与 biz_ref_type 配合使用(例如 orders.id);用于对账与审计
|
||||
Order tenantLedgerQueryBelongsToOrder
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
@@ -99,6 +105,9 @@ func (t *tenantLedgerQuery) updateTableName(table string) *tenantLedgerQuery {
|
||||
t.Remark = field.NewString(table, "remark")
|
||||
t.CreatedAt = field.NewTime(table, "created_at")
|
||||
t.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
t.OperatorUserID = field.NewInt64(table, "operator_user_id")
|
||||
t.BizRefType = field.NewString(table, "biz_ref_type")
|
||||
t.BizRefID = field.NewInt64(table, "biz_ref_id")
|
||||
|
||||
t.fillFieldMap()
|
||||
|
||||
@@ -131,7 +140,7 @@ func (t *tenantLedgerQuery) GetFieldByName(fieldName string) (field.OrderExpr, b
|
||||
}
|
||||
|
||||
func (t *tenantLedgerQuery) fillFieldMap() {
|
||||
t.fieldMap = make(map[string]field.Expr, 15)
|
||||
t.fieldMap = make(map[string]field.Expr, 18)
|
||||
t.fieldMap["id"] = t.ID
|
||||
t.fieldMap["tenant_id"] = t.TenantID
|
||||
t.fieldMap["user_id"] = t.UserID
|
||||
@@ -146,6 +155,9 @@ func (t *tenantLedgerQuery) fillFieldMap() {
|
||||
t.fieldMap["remark"] = t.Remark
|
||||
t.fieldMap["created_at"] = t.CreatedAt
|
||||
t.fieldMap["updated_at"] = t.UpdatedAt
|
||||
t.fieldMap["operator_user_id"] = t.OperatorUserID
|
||||
t.fieldMap["biz_ref_type"] = t.BizRefType
|
||||
t.fieldMap["biz_ref_id"] = t.BizRefID
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user