feat: 移除“租户管理员为用户充值 / 每租户一套余额”能力:余额统一为全局用户余额

This commit is contained in:
2025-12-23 10:59:59 +08:00
parent dd7bcdfb98
commit a80c9b759b
39 changed files with 566 additions and 1869 deletions
+1 -19
View File
@@ -14,7 +14,7 @@ import (
// - 查询/展示时可以先看 kind,再按需解析 data。
// - 兼容历史数据:如果旧数据没有 kind/data,则按 legacy 处理(data = 原始 JSON)。
type OrdersSnapshot struct {
// Kind 快照类型:建议与订单类型对齐(例如 content_purchase / topup)。
// Kind 快照类型:建议与订单类型对齐(例如 content_purchase)。
Kind string `json:"kind"`
// Data 具体快照数据(按 Kind 对应不同结构)。
Data json.RawMessage `json:"data"`
@@ -78,21 +78,3 @@ type OrdersContentPurchaseSnapshot struct {
// PurchasePricingNotes 价格计算补充说明(可选,便于排查争议)。
PurchasePricingNotes string `json:"purchase_pricing_notes,omitempty"`
}
// OrdersTopupSnapshot 为“后台充值订单”的快照(用于审计与追责)。
type OrdersTopupSnapshot struct {
// OperatorUserID 充值操作人用户ID(租户管理员)。
OperatorUserID int64 `json:"operator_user_id"`
// TargetUserID 充值目标用户ID(租户成员)。
TargetUserID int64 `json:"target_user_id"`
// Amount 充值金额(分)。
Amount int64 `json:"amount"`
// Currency 币种:当前固定 CNY(金额单位为分)。
Currency consts.Currency `json:"currency"`
// Reason 充值原因(可选,强烈建议填写用于审计)。
Reason string `json:"reason,omitempty"`
// IdempotencyKey 幂等键(可选)。
IdempotencyKey string `json:"idempotency_key,omitempty"`
// TopupAt 充值时间(逻辑时间)。
TopupAt time.Time `json:"topup_at"`
}
@@ -10,9 +10,17 @@ CREATE TABLE IF NOT EXISTS users(
roles text[] NOT NULL DEFAULT ARRAY['user'],
status varchar(50) NOT NULL DEFAULT 'active',
metas jsonb NOT NULL DEFAULT '{}',
balance bigint NOT NULL DEFAULT 0,
balance_frozen bigint NOT NULL DEFAULT 0,
verified_at timestamptz
);
COMMENT ON COLUMN users.balance IS '全局可用余额:分/最小货币单位;用户在所有已加入租户内共享该余额;默认 0';
COMMENT ON COLUMN users.balance_frozen IS '全局冻结余额:分/最小货币单位;用于下单冻结等;默认 0';
CREATE INDEX IF NOT EXISTS ix_users_balance ON users(balance);
CREATE INDEX IF NOT EXISTS ix_users_balance_frozen ON users(balance_frozen);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
@@ -5,7 +5,6 @@ CREATE TABLE IF NOT EXISTS tenant_users(
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
role TEXT[] NOT NULL DEFAULT ARRAY['member'],
balance bigint NOT NULL DEFAULT 0,
status varchar(50) NOT NULL DEFAULT 'active',
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
@@ -1,13 +1,5 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE tenant_users
ADD COLUMN IF NOT EXISTS balance_frozen bigint NOT NULL DEFAULT 0;
-- tenant_users.balance_frozen:冻结余额(用于下单冻结、争议期等)
COMMENT ON COLUMN tenant_users.balance_frozen IS '冻结余额:分/最小货币单位;下单冻结时从可用余额转入,最终扣款或回滚时转出;默认 0';
CREATE INDEX IF NOT EXISTS ix_tenant_users_tenant_balance_frozen ON tenant_users(tenant_id, balance_frozen);
CREATE TABLE IF NOT EXISTS orders(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
@@ -29,12 +21,12 @@ CREATE TABLE IF NOT EXISTS orders(
updated_at timestamptz NOT NULL DEFAULT NOW()
);
-- orders:订单主表(租户内购买/充值等业务单据)
-- orders:订单主表(租户内购买等业务单据)
COMMENT ON TABLE orders IS '订单:租户内的业务交易单据;记录成交金额快照、状态流转与退款信息;所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN orders.id IS '主键ID:自增;用于关联订单明细、账本流水、权益等';
COMMENT ON COLUMN orders.tenant_id IS '租户ID:多租户隔离关键字段;所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN orders.user_id IS '用户ID:下单用户(buyer);余额扣款与权益归属以该 user_id 为准';
COMMENT ON COLUMN orders.type IS '订单类型:content_purchase(购买内容)/topup(充值)等;当前默认 content_purchase';
COMMENT ON COLUMN orders.type IS '订单类型:content_purchase(购买内容)等;当前默认 content_purchase';
COMMENT ON COLUMN orders.status IS '订单状态:created/paid/refunding/refunded/canceled/failed;状态变更需与账本/权益保持一致';
COMMENT ON COLUMN orders.currency IS '币种:当前固定 CNY;金额单位为分';
COMMENT ON COLUMN orders.amount_original IS '原价金额:分;未折扣前金额(用于展示与对账)';
@@ -103,12 +95,12 @@ CREATE TABLE IF NOT EXISTS tenant_ledgers(
);
-- tenant_ledgers:租户内余额账本流水(必须可审计、可幂等)
COMMENT ON TABLE tenant_ledgers IS '账本流水:记录租户内用户余额的每一次变化(充值/冻结/扣款/退款等);用于审计与对账回放';
COMMENT ON TABLE tenant_ledgers IS '账本流水:记录租户内用户余额的每一次变化(冻结/扣款/退款/调账等);用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.id IS '主键ID:自增';
COMMENT ON COLUMN tenant_ledgers.tenant_id IS '租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致';
COMMENT ON COLUMN tenant_ledgers.user_id IS '用户ID:余额账户归属用户;对应 tenant_users.user_id';
COMMENT ON COLUMN tenant_ledgers.order_id IS '关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空';
COMMENT ON COLUMN tenant_ledgers.type IS '流水类型:credit_topup/debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向';
COMMENT ON COLUMN tenant_ledgers.type IS '流水类型:debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向';
COMMENT ON COLUMN tenant_ledgers.amount IS '流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)';
COMMENT ON COLUMN tenant_ledgers.balance_before IS '变更前可用余额:用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.balance_after IS '变更后可用余额:用于审计与对账回放';
@@ -143,8 +135,4 @@ DROP INDEX IF EXISTS ix_orders_tenant_status;
DROP INDEX IF EXISTS ix_orders_tenant_user;
DROP TABLE IF EXISTS orders;
DROP INDEX IF EXISTS ix_tenant_users_tenant_balance_frozen;
ALTER TABLE tenant_users DROP COLUMN IF EXISTS balance_frozen;
-- +goose StatementEnd
@@ -6,12 +6,12 @@ ALTER TABLE tenant_ledgers
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 组成可选的结构化幂等/追溯键';
-- 用途:在 idempotency_key 之外提供结构化引用(例如 order/refund 等),便于报表与按业务对象追溯。
COMMENT ON COLUMN tenant_ledgers.biz_ref_type IS '业务引用类型:order/refund/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键';
COMMENT ON COLUMN tenant_ledgers.biz_ref_id IS '业务引用ID:与 biz_ref_type 配合使用(例如 orders.id);用于对账与审计';
-- 索引:按操作者检索敏感操作流水(后台审计用)。
@@ -38,4 +38,3 @@ ALTER TABLE tenant_ledgers
DROP COLUMN IF EXISTS biz_ref_type,
DROP COLUMN IF EXISTS operator_user_id;
-- +goose StatementEnd
@@ -0,0 +1,24 @@
-- +goose Up
-- +goose StatementBegin
-- 清理“充值”遗留描述:当前项目已移除租户充值与 per-tenant 余额。
COMMENT ON COLUMN orders.type IS '订单类型:content_purchase(购买内容)等;当前默认 content_purchase';
COMMENT ON TABLE tenant_ledgers IS '账本流水:记录租户内用户余额的每一次变化(冻结/扣款/退款/调账等);用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.type IS '流水类型:debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向';
COMMENT ON COLUMN tenant_ledgers.operator_user_id IS '操作者用户ID:谁触发该流水(admin/buyer/system);用于审计与追责;可为空(历史数据或无法识别时)';
COMMENT ON COLUMN tenant_ledgers.biz_ref_type IS '业务引用类型:order/refund/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- 新项目不需要依赖 Down 做历史回滚;保持与 Up 一致,避免引入已移除特性的遗留描述。
COMMENT ON COLUMN orders.type IS '订单类型:content_purchase(购买内容)等;当前默认 content_purchase';
COMMENT ON TABLE tenant_ledgers IS '账本流水:记录租户内用户余额的每一次变化(冻结/扣款/退款/调账等);用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.type IS '流水类型:debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向';
COMMENT ON COLUMN tenant_ledgers.operator_user_id IS '操作者用户ID:谁触发该流水(admin/buyer/system);用于审计与追责;可为空(历史数据或无法识别时)';
COMMENT ON COLUMN tenant_ledgers.biz_ref_type IS '业务引用类型:order/refund/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键';
-- +goose StatementEnd
+1 -1
View File
@@ -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()
Content *Content `gorm:"foreignKey:ContentID;references:ID" json:"content,omitempty"`
Order *Order `gorm:"foreignKey:OrderID;references:ID" json:"order,omitempty"`
Content *Content `gorm:"foreignKey:ContentID;references:ID" json:"content,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.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.Content = orderItemQueryBelongsToContent{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Content", "Content"),
}
_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()
Content orderItemQueryBelongsToContent
Order orderItemQueryBelongsToOrder
Order orderItemQueryBelongsToOrder
Content orderItemQueryBelongsToContent
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.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
o.Content.db = db.Session(&gorm.Session{Initialized: true})
o.Content.db.Statement.ConnPool = db.Statement.ConnPool
return o
}
func (o orderItemQuery) replaceDB(db *gorm.DB) orderItemQuery {
o.orderItemQueryDo.ReplaceDB(db)
o.Content.db = db.Session(&gorm.Session{})
o.Order.db = db.Session(&gorm.Session{})
o.Content.db = db.Session(&gorm.Session{})
return o
}
type orderItemQueryBelongsToContent struct {
db *gorm.DB
field.RelationField
}
func (a orderItemQueryBelongsToContent) Where(conds ...field.Expr) *orderItemQueryBelongsToContent {
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 orderItemQueryBelongsToContent) WithContext(ctx context.Context) *orderItemQueryBelongsToContent {
a.db = a.db.WithContext(ctx)
return &a
}
func (a orderItemQueryBelongsToContent) Session(session *gorm.Session) *orderItemQueryBelongsToContent {
a.db = a.db.Session(session)
return &a
}
func (a orderItemQueryBelongsToContent) Model(m *OrderItem) *orderItemQueryBelongsToContentTx {
return &orderItemQueryBelongsToContentTx{a.db.Model(m).Association(a.Name())}
}
func (a orderItemQueryBelongsToContent) Unscoped() *orderItemQueryBelongsToContent {
a.db = a.db.Unscoped()
return &a
}
type orderItemQueryBelongsToContentTx struct{ tx *gorm.Association }
func (a orderItemQueryBelongsToContentTx) Find() (result *Content, err error) {
return result, a.tx.Find(&result)
}
func (a orderItemQueryBelongsToContentTx) Append(values ...*Content) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a orderItemQueryBelongsToContentTx) Replace(values ...*Content) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a orderItemQueryBelongsToContentTx) Delete(values ...*Content) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a orderItemQueryBelongsToContentTx) Clear() error {
return a.tx.Clear()
}
func (a orderItemQueryBelongsToContentTx) Count() int64 {
return a.tx.Count()
}
func (a orderItemQueryBelongsToContentTx) Unscoped() *orderItemQueryBelongsToContentTx {
a.tx = a.tx.Unscoped()
return &a
}
type orderItemQueryBelongsToOrder struct {
db *gorm.DB
@@ -319,6 +238,87 @@ func (a orderItemQueryBelongsToOrderTx) Unscoped() *orderItemQueryBelongsToOrder
return &a
}
type orderItemQueryBelongsToContent struct {
db *gorm.DB
field.RelationField
}
func (a orderItemQueryBelongsToContent) Where(conds ...field.Expr) *orderItemQueryBelongsToContent {
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 orderItemQueryBelongsToContent) WithContext(ctx context.Context) *orderItemQueryBelongsToContent {
a.db = a.db.WithContext(ctx)
return &a
}
func (a orderItemQueryBelongsToContent) Session(session *gorm.Session) *orderItemQueryBelongsToContent {
a.db = a.db.Session(session)
return &a
}
func (a orderItemQueryBelongsToContent) Model(m *OrderItem) *orderItemQueryBelongsToContentTx {
return &orderItemQueryBelongsToContentTx{a.db.Model(m).Association(a.Name())}
}
func (a orderItemQueryBelongsToContent) Unscoped() *orderItemQueryBelongsToContent {
a.db = a.db.Unscoped()
return &a
}
type orderItemQueryBelongsToContentTx struct{ tx *gorm.Association }
func (a orderItemQueryBelongsToContentTx) Find() (result *Content, err error) {
return result, a.tx.Find(&result)
}
func (a orderItemQueryBelongsToContentTx) Append(values ...*Content) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a orderItemQueryBelongsToContentTx) Replace(values ...*Content) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a orderItemQueryBelongsToContentTx) Delete(values ...*Content) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a orderItemQueryBelongsToContentTx) Clear() error {
return a.tx.Clear()
}
func (a orderItemQueryBelongsToContentTx) Count() int64 {
return a.tx.Count()
}
func (a orderItemQueryBelongsToContentTx) Unscoped() *orderItemQueryBelongsToContentTx {
a.tx = a.tx.Unscoped()
return &a
}
type orderItemQueryDo struct{ gen.DO }
func (o orderItemQueryDo) Debug() *orderItemQueryDo {
+1 -1
View File
@@ -22,7 +22,7 @@ type Order struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true;comment:主键ID:自增;用于关联订单明细、账本流水、权益等" json:"id"` // 主键ID:自增;用于关联订单明细、账本流水、权益等
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null;comment:租户ID:多租户隔离关键字段;所有查询/写入必须限定 tenant_id" json:"tenant_id"` // 租户ID:多租户隔离关键字段;所有查询/写入必须限定 tenant_id
UserID int64 `gorm:"column:user_id;type:bigint;not null;comment:用户ID:下单用户(buyer);余额扣款与权益归属以该 user_id 为准" json:"user_id"` // 用户ID:下单用户(buyer);余额扣款与权益归属以该 user_id 为准
Type consts.OrderType `gorm:"column:type;type:character varying(32);not null;default:content_purchase;comment:订单类型:content_purchase(购买内容)/topup(充值)等;当前默认 content_purchase" json:"type"` // 订单类型:content_purchase(购买内容)/topup(充值)等;当前默认 content_purchase
Type consts.OrderType `gorm:"column:type;type:character varying(32);not null;default:content_purchase;comment:订单类型:content_purchase(购买内容)等;当前默认 content_purchase" json:"type"` // 订单类型:content_purchase(购买内容)等;当前默认 content_purchase
Status consts.OrderStatus `gorm:"column:status;type:character varying(32);not null;default:created;comment:订单状态:created/paid/refunding/refunded/canceled/failed;状态变更需与账本/权益保持一致" json:"status"` // 订单状态:created/paid/refunding/refunded/canceled/failed;状态变更需与账本/权益保持一致
Currency consts.Currency `gorm:"column:currency;type:character varying(16);not null;default:CNY;comment:币种:当前固定 CNY;金额单位为分" json:"currency"` // 币种:当前固定 CNY;金额单位为分
AmountOriginal int64 `gorm:"column:amount_original;type:bigint;not null;comment:原价金额:分;未折扣前金额(用于展示与对账)" json:"amount_original"` // 原价金额:分;未折扣前金额(用于展示与对账)
+1 -1
View File
@@ -61,7 +61,7 @@ type orderQuery struct {
ID field.Int64 // 主键ID:自增;用于关联订单明细、账本流水、权益等
TenantID field.Int64 // 租户ID:多租户隔离关键字段;所有查询/写入必须限定 tenant_id
UserID field.Int64 // 用户ID:下单用户(buyer);余额扣款与权益归属以该 user_id 为准
Type field.Field // 订单类型:content_purchase(购买内容)/topup(充值)等;当前默认 content_purchase
Type field.Field // 订单类型:content_purchase(购买内容)等;当前默认 content_purchase
Status field.Field // 订单状态:created/paid/refunding/refunded/canceled/failed;状态变更需与账本/权益保持一致
Currency field.Field // 币种:当前固定 CNY;金额单位为分
AmountOriginal field.Int64 // 原价金额:分;未折扣前金额(用于展示与对账)
+17 -17
View File
@@ -17,23 +17,23 @@ const TableNameTenantLedger = "tenant_ledgers"
// TenantLedger mapped from table <tenant_ledgers>
type TenantLedger struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true;comment:主键ID:自增" json:"id"` // 主键ID:自增
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null;comment:租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致" json:"tenant_id"` // 租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致
UserID int64 `gorm:"column:user_id;type:bigint;not null;comment:用户ID:余额账户归属用户;对应 tenant_users.user_id" json:"user_id"` // 用户ID:余额账户归属用户;对应 tenant_users.user_id
OrderID int64 `gorm:"column:order_id;type:bigint;comment:关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空" json:"order_id"` // 关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空
Type consts.TenantLedgerType `gorm:"column:type;type:character varying(32);not null;comment:流水类型:credit_topup/debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向" json:"type"` // 流水类型:credit_topup/debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)" json:"amount"` // 流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)
BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null;comment:变更前可用余额:用于审计与对账回放" json:"balance_before"` // 变更前可用余额:用于审计与对账回放
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null;comment:变更后可用余额:用于审计与对账回放" json:"balance_after"` // 变更后可用余额:用于审计与对账回放
FrozenBefore int64 `gorm:"column:frozen_before;type:bigint;not null;comment:变更前冻结余额:用于审计与对账回放" json:"frozen_before"` // 变更前冻结余额:用于审计与对账回放
FrozenAfter int64 `gorm:"column:frozen_after;type:bigint;not null;comment:变更后冻结余额:用于审计与对账回放" json:"frozen_after"` // 变更后冻结余额:用于审计与对账回放
IdempotencyKey string `gorm:"column:idempotency_key;type:character varying(128);not null;comment:幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)" json:"idempotency_key"` // 幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)
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);用于对账与审计
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true;comment:主键ID:自增" json:"id"` // 主键ID:自增
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null;comment:租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致" json:"tenant_id"` // 租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致
UserID int64 `gorm:"column:user_id;type:bigint;not null;comment:用户ID:余额账户归属用户;对应 tenant_users.user_id" json:"user_id"` // 用户ID:余额账户归属用户;对应 tenant_users.user_id
OrderID int64 `gorm:"column:order_id;type:bigint;comment:关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空" json:"order_id"` // 关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空
Type consts.TenantLedgerType `gorm:"column:type;type:character varying(32);not null;comment:流水类型:debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向" json:"type"` // 流水类型:debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)" json:"amount"` // 流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)
BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null;comment:变更前可用余额:用于审计与对账回放" json:"balance_before"` // 变更前可用余额:用于审计与对账回放
BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null;comment:变更后可用余额:用于审计与对账回放" json:"balance_after"` // 变更后可用余额:用于审计与对账回放
FrozenBefore int64 `gorm:"column:frozen_before;type:bigint;not null;comment:变更前冻结余额:用于审计与对账回放" json:"frozen_before"` // 变更前冻结余额:用于审计与对账回放
FrozenAfter int64 `gorm:"column:frozen_after;type:bigint;not null;comment:变更后冻结余额:用于审计与对账回放" json:"frozen_after"` // 变更后冻结余额:用于审计与对账回放
IdempotencyKey string `gorm:"column:idempotency_key;type:character varying(128);not null;comment:幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)" json:"idempotency_key"` // 幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)
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/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键" json:"biz_ref_type"` // 业务引用类型:order/refund/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"`
}
@@ -61,7 +61,7 @@ type tenantLedgerQuery struct {
TenantID field.Int64 // 租户ID:多租户隔离关键字段;必须与 tenant_users.tenant_id 一致
UserID field.Int64 // 用户ID:余额账户归属用户;对应 tenant_users.user_id
OrderID field.Int64 // 关联订单ID:购买/退款类流水应关联 orders.id;非订单类可为空
Type field.Field // 流水类型:credit_topup/debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向
Type field.Field // 流水类型:debit_purchase/credit_refund/freeze/unfreeze/adjustment;不同类型决定余额/冻结余额的变更方向
Amount field.Int64 // 流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)
BalanceBefore field.Int64 // 变更前可用余额:用于审计与对账回放
BalanceAfter field.Int64 // 变更后可用余额:用于审计与对账回放
@@ -72,7 +72,7 @@ type tenantLedgerQuery struct {
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 组成可选的结构化幂等/追溯键
BizRefType field.String // 业务引用类型:order/refund/etc;与 biz_ref_id 组成可选的结构化幂等/追溯键
BizRefID field.Int64 // 业务引用ID:与 biz_ref_type 配合使用(例如 orders.id);用于对账与审计
Order tenantLedgerQueryBelongsToOrder
+7 -9
View File
@@ -18,15 +18,13 @@ const TableNameTenantUser = "tenant_users"
// TenantUser mapped from table <tenant_users>
type TenantUser struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Role types.Array[consts.TenantUserRole] `gorm:"column:role;type:text[];not null;default:ARRAY['member" json:"role"`
Balance int64 `gorm:"column:balance;type:bigint;not null" json:"balance"`
Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:verified" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"`
BalanceFrozen int64 `gorm:"column:balance_frozen;type:bigint;not null;comment:冻结余额:分/最小货币单位;下单冻结时从可用余额转入,最终扣款或回滚时转出;默认 0" json:"balance_frozen"` // 冻结余额:分/最小货币单位;下单冻结时从可用余额转入,最终扣款或回滚时转出;默认 0
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Role types.Array[consts.TenantUserRole] `gorm:"column:role;type:text[];not null;default:ARRAY['member" json:"role"`
Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:verified" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"`
}
// Quick operations without importing query package
@@ -29,11 +29,9 @@ func newTenantUser(db *gorm.DB, opts ...gen.DOOption) tenantUserQuery {
_tenantUserQuery.TenantID = field.NewInt64(tableName, "tenant_id")
_tenantUserQuery.UserID = field.NewInt64(tableName, "user_id")
_tenantUserQuery.Role = field.NewArray(tableName, "role")
_tenantUserQuery.Balance = field.NewInt64(tableName, "balance")
_tenantUserQuery.Status = field.NewField(tableName, "status")
_tenantUserQuery.CreatedAt = field.NewTime(tableName, "created_at")
_tenantUserQuery.UpdatedAt = field.NewTime(tableName, "updated_at")
_tenantUserQuery.BalanceFrozen = field.NewInt64(tableName, "balance_frozen")
_tenantUserQuery.fillFieldMap()
@@ -43,16 +41,14 @@ func newTenantUser(db *gorm.DB, opts ...gen.DOOption) tenantUserQuery {
type tenantUserQuery struct {
tenantUserQueryDo tenantUserQueryDo
ALL field.Asterisk
ID field.Int64
TenantID field.Int64
UserID field.Int64
Role field.Array
Balance field.Int64
Status field.Field
CreatedAt field.Time
UpdatedAt field.Time
BalanceFrozen field.Int64 // 冻结余额:分/最小货币单位;下单冻结时从可用余额转入,最终扣款或回滚时转出;默认 0
ALL field.Asterisk
ID field.Int64
TenantID field.Int64
UserID field.Int64
Role field.Array
Status field.Field
CreatedAt field.Time
UpdatedAt field.Time
fieldMap map[string]field.Expr
}
@@ -73,11 +69,9 @@ func (t *tenantUserQuery) updateTableName(table string) *tenantUserQuery {
t.TenantID = field.NewInt64(table, "tenant_id")
t.UserID = field.NewInt64(table, "user_id")
t.Role = field.NewArray(table, "role")
t.Balance = field.NewInt64(table, "balance")
t.Status = field.NewField(table, "status")
t.CreatedAt = field.NewTime(table, "created_at")
t.UpdatedAt = field.NewTime(table, "updated_at")
t.BalanceFrozen = field.NewInt64(table, "balance_frozen")
t.fillFieldMap()
@@ -110,16 +104,14 @@ func (t *tenantUserQuery) GetFieldByName(fieldName string) (field.OrderExpr, boo
}
func (t *tenantUserQuery) fillFieldMap() {
t.fieldMap = make(map[string]field.Expr, 9)
t.fieldMap = make(map[string]field.Expr, 7)
t.fieldMap["id"] = t.ID
t.fieldMap["tenant_id"] = t.TenantID
t.fieldMap["user_id"] = t.UserID
t.fieldMap["role"] = t.Role
t.fieldMap["balance"] = t.Balance
t.fieldMap["status"] = t.Status
t.fieldMap["created_at"] = t.CreatedAt
t.fieldMap["updated_at"] = t.UpdatedAt
t.fieldMap["balance_frozen"] = t.BalanceFrozen
}
func (t tenantUserQuery) clone(db *gorm.DB) tenantUserQuery {
+14 -12
View File
@@ -19,18 +19,20 @@ const TableNameUser = "users"
// User mapped from table <users>
type User struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"`
Username string `gorm:"column:username;type:character varying(255);not null" json:"username"`
Password string `gorm:"column:password;type:character varying(255);not null" json:"password"`
Roles types.Array[consts.Role] `gorm:"column:roles;type:text[];not null;default:ARRAY['user" json:"roles"`
Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:active" json:"status"`
Metas types.JSON `gorm:"column:metas;type:jsonb;not null;default:{}" json:"metas"`
VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp with time zone" json:"verified_at"`
OwnedTenant *Tenant `json:"owned,omitempty"`
Tenants []*Tenant `gorm:"joinForeignKey:UserID;joinReferences:TenantID;many2many:tenant_users" json:"tenants,omitempty"`
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"`
Username string `gorm:"column:username;type:character varying(255);not null" json:"username"`
Password string `gorm:"column:password;type:character varying(255);not null" json:"password"`
Roles types.Array[consts.Role] `gorm:"column:roles;type:text[];not null;default:ARRAY['user" json:"roles"`
Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:active" json:"status"`
Metas types.JSON `gorm:"column:metas;type:jsonb;not null;default:{}" json:"metas"`
Balance int64 `gorm:"column:balance;type:bigint;not null;comment:全局可用余额:分/最小货币单位;用户在所有已加入租户内共享该余额;默认 0" json:"balance"` // 全局可用余额:分/最小货币单位;用户在所有已加入租户内共享该余额;默认 0
BalanceFrozen int64 `gorm:"column:balance_frozen;type:bigint;not null;comment:全局冻结余额:分/最小货币单位;用于下单冻结等;默认 0" json:"balance_frozen"` // 全局冻结余额:分/最小货币单位;用于下单冻结等;默认 0
VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp with time zone" json:"verified_at"`
OwnedTenant *Tenant `json:"owned,omitempty"`
Tenants []*Tenant `gorm:"joinForeignKey:UserID;joinReferences:TenantID;many2many:tenant_users" json:"tenants,omitempty"`
}
// Quick operations without importing query package
+21 -13
View File
@@ -34,6 +34,8 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery {
_userQuery.Roles = field.NewArray(tableName, "roles")
_userQuery.Status = field.NewField(tableName, "status")
_userQuery.Metas = field.NewJSONB(tableName, "metas")
_userQuery.Balance = field.NewInt64(tableName, "balance")
_userQuery.BalanceFrozen = field.NewInt64(tableName, "balance_frozen")
_userQuery.VerifiedAt = field.NewTime(tableName, "verified_at")
_userQuery.OwnedTenant = userQueryBelongsToOwnedTenant{
db: db.Session(&gorm.Session{}),
@@ -55,18 +57,20 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery {
type userQuery struct {
userQueryDo userQueryDo
ALL field.Asterisk
ID field.Int64
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Username field.String
Password field.String
Roles field.Array
Status field.Field
Metas field.JSONB
VerifiedAt field.Time
OwnedTenant userQueryBelongsToOwnedTenant
ALL field.Asterisk
ID field.Int64
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Username field.String
Password field.String
Roles field.Array
Status field.Field
Metas field.JSONB
Balance field.Int64 // 全局可用余额:分/最小货币单位;用户在所有已加入租户内共享该余额;默认 0
BalanceFrozen field.Int64 // 全局冻结余额:分/最小货币单位;用于下单冻结等;默认 0
VerifiedAt field.Time
OwnedTenant userQueryBelongsToOwnedTenant
Tenants userQueryManyToManyTenants
@@ -94,6 +98,8 @@ func (u *userQuery) updateTableName(table string) *userQuery {
u.Roles = field.NewArray(table, "roles")
u.Status = field.NewField(table, "status")
u.Metas = field.NewJSONB(table, "metas")
u.Balance = field.NewInt64(table, "balance")
u.BalanceFrozen = field.NewInt64(table, "balance_frozen")
u.VerifiedAt = field.NewTime(table, "verified_at")
u.fillFieldMap()
@@ -125,7 +131,7 @@ func (u *userQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (u *userQuery) fillFieldMap() {
u.fieldMap = make(map[string]field.Expr, 12)
u.fieldMap = make(map[string]field.Expr, 14)
u.fieldMap["id"] = u.ID
u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt
@@ -135,6 +141,8 @@ func (u *userQuery) fillFieldMap() {
u.fieldMap["roles"] = u.Roles
u.fieldMap["status"] = u.Status
u.fieldMap["metas"] = u.Metas
u.fieldMap["balance"] = u.Balance
u.fieldMap["balance_frozen"] = u.BalanceFrozen
u.fieldMap["verified_at"] = u.VerifiedAt
}