修复气站与配送点支付记录数据范围
This commit is contained in:
68
backend/api/internal/logic/common/payment_scope.go
Normal file
68
backend/api/internal/logic/common/payment_scope.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// 功能:统一气站与配送点管理端的支付记录数据范围过滤。
|
||||
// 版本:v1.0。
|
||||
package common
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
const gasPaymentOrderScopeSQL = `
|
||||
(
|
||||
(payment_order.business_type = 'gasorder' AND EXISTS (
|
||||
SELECT 1 FROM gasorder_basic
|
||||
WHERE gasorder_basic.identity = payment_order.business_identity
|
||||
AND gasorder_basic.gas_basic_id = ?
|
||||
))
|
||||
OR (payment_order.business_type = 'ec_order' AND EXISTS (
|
||||
SELECT 1 FROM ec_order
|
||||
WHERE ec_order.identity = payment_order.business_identity
|
||||
AND ec_order.gas_station_id = ?
|
||||
))
|
||||
OR (payment_order.business_type = 'recharge' AND EXISTS (
|
||||
SELECT 1 FROM wallet_recharge_order
|
||||
JOIN wallet_basic ON wallet_basic.id = wallet_recharge_order.wallet_basic_id
|
||||
WHERE wallet_recharge_order.identity = payment_order.business_identity
|
||||
AND wallet_recharge_order.owner_type = 'gas'
|
||||
AND wallet_recharge_order.owner_identity = ?
|
||||
AND wallet_basic.owner_type = 'gas'
|
||||
AND wallet_basic.owner_id = ?
|
||||
AND wallet_basic.owner_identity = ?
|
||||
))
|
||||
)`
|
||||
|
||||
const deliveryPaymentOrderScopeSQL = `
|
||||
(
|
||||
(payment_order.business_type = 'gasorder' AND EXISTS (
|
||||
SELECT 1 FROM gasorder_basic
|
||||
WHERE gasorder_basic.identity = payment_order.business_identity
|
||||
AND gasorder_basic.delivery_basic_id = ?
|
||||
))
|
||||
OR (payment_order.business_type = 'ec_order' AND EXISTS (
|
||||
SELECT 1 FROM ec_order
|
||||
WHERE ec_order.identity = payment_order.business_identity
|
||||
AND ec_order.delivery_point_id = ?
|
||||
))
|
||||
OR (payment_order.business_type = 'recharge' AND EXISTS (
|
||||
SELECT 1 FROM wallet_recharge_order
|
||||
JOIN wallet_basic ON wallet_basic.id = wallet_recharge_order.wallet_basic_id
|
||||
WHERE wallet_recharge_order.identity = payment_order.business_identity
|
||||
AND wallet_recharge_order.owner_type = 'delivery'
|
||||
AND wallet_recharge_order.owner_identity = ?
|
||||
AND wallet_basic.owner_type = 'delivery'
|
||||
AND wallet_basic.owner_id = ?
|
||||
AND wallet_basic.owner_identity = ?
|
||||
))
|
||||
)`
|
||||
|
||||
// ScopePaymentOrdersByOwner 按业务对象验证统一支付单的组织归属。
|
||||
// ownerType 仅接受 gas 或 delivery;ownerID 为组织内部主键,ownerIdentity 为组织业务标识。
|
||||
// 返回值沿用传入查询并追加失败关闭的数据范围条件,未知类型和孤立业务对象不会被放行。
|
||||
func ScopePaymentOrdersByOwner(query *gorm.DB, ownerType string, ownerID uint64, ownerIdentity string) *gorm.DB {
|
||||
switch ownerType {
|
||||
case "gas":
|
||||
return query.Where(gasPaymentOrderScopeSQL, ownerID, ownerID, ownerIdentity, ownerID, ownerIdentity)
|
||||
case "delivery":
|
||||
return query.Where(deliveryPaymentOrderScopeSQL, ownerID, ownerID, ownerIdentity, ownerID, ownerIdentity)
|
||||
default:
|
||||
// 未知组织类型没有可靠归属链路,必须按失败关闭处理。
|
||||
return query.Where("1 = 0")
|
||||
}
|
||||
}
|
||||
107
backend/api/internal/logic/common/payment_scope_test.go
Normal file
107
backend/api/internal/logic/common/payment_scope_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// 功能:验证气站与配送点支付记录的数据范围 SQL 和失败关闭行为。
|
||||
// 版本:v1.0。
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// paymentScopeSQL 生成 PostgreSQL 方言下的支付范围查询,供各边界用例断言。
|
||||
func paymentScopeSQL(t *testing.T, ownerType string, ownerID uint64, ownerIdentity string, paymentIdentity string) string {
|
||||
t.Helper()
|
||||
connection, _, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = connection.Close() })
|
||||
database, err := gorm.Open(postgres.New(postgres.Config{Conn: connection}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return database.ToSQL(func(tx *gorm.DB) *gorm.DB {
|
||||
query := ScopePaymentOrdersByOwner(ActiveRecords(tx.Model(&models.PaymentOrder{})), ownerType, ownerID, ownerIdentity)
|
||||
if paymentIdentity != "" {
|
||||
query = query.Where("payment_order.identity = ?", paymentIdentity)
|
||||
}
|
||||
return query.Find(&[]models.PaymentOrder{})
|
||||
})
|
||||
}
|
||||
|
||||
// assertSQLContains 校验查询必须包含所有数据范围片段。
|
||||
func assertSQLContains(t *testing.T, statement string, fragments ...string) {
|
||||
t.Helper()
|
||||
for _, fragment := range fragments {
|
||||
if !strings.Contains(statement, fragment) {
|
||||
t.Fatalf("支付范围查询缺少 %q:%s", fragment, statement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestScopePaymentOrdersByGas 验证三类支付只能通过当前气站业务对象进入结果集。
|
||||
func TestScopePaymentOrdersByGas(t *testing.T) {
|
||||
statement := paymentScopeSQL(t, "gas", 11, "gas-identity", "")
|
||||
assertSQLContains(t, statement,
|
||||
`payment_order.business_type = 'gasorder'`,
|
||||
`gasorder_basic.gas_basic_id = 11`,
|
||||
`payment_order.business_type = 'ec_order'`,
|
||||
`ec_order.gas_station_id = 11`,
|
||||
`payment_order.business_type = 'recharge'`,
|
||||
`wallet_recharge_order.owner_type = 'gas'`,
|
||||
`wallet_recharge_order.owner_identity = 'gas-identity'`,
|
||||
`wallet_basic.owner_type = 'gas'`,
|
||||
`wallet_basic.owner_id = 11`,
|
||||
`wallet_basic.owner_identity = 'gas-identity'`,
|
||||
)
|
||||
if strings.Contains(statement, "payment_order.wallet_basic_id") {
|
||||
t.Fatalf("统一支付单仍错误依赖钱包主键:%s", statement)
|
||||
}
|
||||
if strings.Contains(statement, "delivery_basic_id") || strings.Contains(statement, "delivery_point_id") {
|
||||
t.Fatalf("气站范围查询混入配送点归属字段:%s", statement)
|
||||
}
|
||||
}
|
||||
|
||||
// TestScopePaymentOrdersByDelivery 验证三类支付只能通过当前配送点业务对象进入结果集。
|
||||
func TestScopePaymentOrdersByDelivery(t *testing.T) {
|
||||
statement := paymentScopeSQL(t, "delivery", 22, "delivery-identity", "")
|
||||
assertSQLContains(t, statement,
|
||||
`payment_order.business_type = 'gasorder'`,
|
||||
`gasorder_basic.delivery_basic_id = 22`,
|
||||
`payment_order.business_type = 'ec_order'`,
|
||||
`ec_order.delivery_point_id = 22`,
|
||||
`payment_order.business_type = 'recharge'`,
|
||||
`wallet_recharge_order.owner_type = 'delivery'`,
|
||||
`wallet_recharge_order.owner_identity = 'delivery-identity'`,
|
||||
`wallet_basic.owner_type = 'delivery'`,
|
||||
`wallet_basic.owner_id = 22`,
|
||||
`wallet_basic.owner_identity = 'delivery-identity'`,
|
||||
)
|
||||
if strings.Contains(statement, "payment_order.wallet_basic_id") {
|
||||
t.Fatalf("统一支付单仍错误依赖钱包主键:%s", statement)
|
||||
}
|
||||
if strings.Contains(statement, "gasorder_basic.gas_basic_id") || strings.Contains(statement, "ec_order.gas_station_id") {
|
||||
t.Fatalf("配送点范围查询混入气站归属字段:%s", statement)
|
||||
}
|
||||
}
|
||||
|
||||
// TestScopePaymentOrdersByOwnerFailsClosed 验证未知组织类型不会放行任何支付记录。
|
||||
func TestScopePaymentOrdersByOwnerFailsClosed(t *testing.T) {
|
||||
statement := paymentScopeSQL(t, "unknown", 33, "unknown-identity", "")
|
||||
assertSQLContains(t, statement, "1 = 0")
|
||||
}
|
||||
|
||||
// TestScopePaymentOrderDetailKeepsOwnerBoundary 验证详情定位不会绕过与列表相同的组织范围。
|
||||
func TestScopePaymentOrderDetailKeepsOwnerBoundary(t *testing.T) {
|
||||
statement := paymentScopeSQL(t, "delivery", 44, "delivery-detail", "payment-detail")
|
||||
assertSQLContains(t, statement,
|
||||
`gasorder_basic.delivery_basic_id = 44`,
|
||||
`ec_order.delivery_point_id = 44`,
|
||||
`wallet_basic.owner_identity = 'delivery-detail'`,
|
||||
`payment_order.identity = 'payment-detail'`,
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// 功能:提供配送点管理端钱包、支付、充值、提现及结算接口。
|
||||
// 版本:v1.1。
|
||||
package delivery
|
||||
|
||||
import (
|
||||
@@ -56,9 +58,23 @@ func listWalletChild(ctx *gin.Context, model any, table string) {
|
||||
}
|
||||
|
||||
func ListBank(ctx *gin.Context) { listWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func ListPayment(ctx *gin.Context) { listWalletChild(ctx, &models.PaymentOrder{}, "payment_order") }
|
||||
func ListRecord(ctx *gin.Context) { listWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func ListRefund(ctx *gin.Context) { listWalletChild(ctx, &models.PaymentRefund{}, "payment_refund") }
|
||||
|
||||
// paymentOrderQuery 按统一支付单关联的业务对象限定当前配送点数据范围。
|
||||
func paymentOrderQuery(point models.DeliveryBasic) *gorm.DB {
|
||||
query := common.ActiveRecords(db().Model(&models.PaymentOrder{}))
|
||||
return common.ScopePaymentOrdersByOwner(query, "delivery", point.ID, point.Identity)
|
||||
}
|
||||
|
||||
// ListPayment 返回当前配送点业务范围内的统一支付记录。
|
||||
func ListPayment(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
listScoped(ctx, &models.PaymentOrder{}, paymentOrderQuery(point), "payment_order.created_at desc")
|
||||
}
|
||||
func ListRecharge(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
@@ -87,9 +103,18 @@ func getWalletChild(ctx *gin.Context, model any, table string) {
|
||||
}
|
||||
|
||||
func GetBank(ctx *gin.Context) { getWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func GetPayment(ctx *gin.Context) { getWalletChild(ctx, &models.PaymentOrder{}, "payment_order") }
|
||||
func GetRecord(ctx *gin.Context) { getWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func GetRefund(ctx *gin.Context) { getWalletChild(ctx, &models.PaymentRefund{}, "payment_refund") }
|
||||
|
||||
// GetPayment 返回当前配送点业务范围内的单条统一支付记录。
|
||||
func GetPayment(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := paymentOrderQuery(point).Where("payment_order.identity = ?", ctx.Param("identity"))
|
||||
respondRecord(ctx, query, &models.PaymentOrder{})
|
||||
}
|
||||
func GetRecharge(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// 功能:提供气站管理端钱包、支付、退款、提现、结算及对账接口。
|
||||
// 版本:v1.1。
|
||||
package gas
|
||||
|
||||
import (
|
||||
@@ -70,11 +72,30 @@ func getWalletChild(ctx *gin.Context, model any, table string) {
|
||||
|
||||
func ListWalletBank(ctx *gin.Context) { listWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func GetWalletBank(ctx *gin.Context) { getWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func ListPaymentOrder(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.PaymentOrder{}, "payment_order")
|
||||
|
||||
// paymentOrderQuery 按统一支付单关联的业务对象限定当前气站数据范围。
|
||||
func paymentOrderQuery(station models.GasBasic) *gorm.DB {
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.PaymentOrder{}))
|
||||
return common.ScopePaymentOrdersByOwner(query, "gas", station.ID, station.Identity)
|
||||
}
|
||||
|
||||
// ListPaymentOrder 返回当前气站业务范围内的统一支付记录。
|
||||
func ListPaymentOrder(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
listScoped(ctx, &models.PaymentOrder{}, paymentOrderQuery(station), "payment_order.created_at desc")
|
||||
}
|
||||
|
||||
// GetPaymentOrder 返回当前气站业务范围内的单条统一支付记录。
|
||||
func GetPaymentOrder(ctx *gin.Context) {
|
||||
getWalletChild(ctx, &models.PaymentOrder{}, "payment_order")
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := paymentOrderQuery(station).Where("payment_order.identity = ?", ctx.Param("identity"))
|
||||
respondScopedRecord(ctx, query, &models.PaymentOrder{})
|
||||
}
|
||||
func ListWalletRecord(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletRecord{}, "wallet_record")
|
||||
|
||||
@@ -122,6 +122,8 @@
|
||||
### 4.7 财务管理
|
||||
|
||||
- 钱包、银行卡、支付记录、钱包流水、退款记录、结算记录只读。
|
||||
- `payment_order` 是统一支付尝试事实,气站端必须通过 `business_type + business_identity` 关联燃气配送订单、商城订单或气站自有钱包充值单,并验证业务对象属于当前气站;不得假设支付单直接包含钱包主键。
|
||||
- 未知业务类型、业务对象缺失以及用户或工作人员钱包充值无法证明属于当前气站,不得向气站端返回。钱包余额支付当前记录在 `wallet_record`,不伪装为统一支付单。
|
||||
- 气站可以创建提现申请并查看处理结果。
|
||||
- 气站不能充值、冻结钱包,不能审批、驳回或标记提现完成。
|
||||
- 所有金额使用最小货币单位整数,服务端校验余额和归属。
|
||||
@@ -153,7 +155,8 @@
|
||||
| `gasorder_contract_product` | `append_only` | 解绑使用专用动作 |
|
||||
| `gasorder_contract_revision` | `readonly` | 不可变历史 |
|
||||
| `gasorder_basic` | `append_only` | 创建及专用状态动作 |
|
||||
| `wallet_basic`、`wallet_bank`、`payment_order`、`wallet_record`、`payment_refund` | `readonly` | 当前气站钱包范围 |
|
||||
| `wallet_basic`、`wallet_bank`、`wallet_record`、`payment_refund` | `readonly` | 当前气站钱包范围 |
|
||||
| `payment_order` | `readonly` | 通过业务对象验证的当前气站统一支付记录 |
|
||||
| `wallet_apply_cash` | `append_only` | 只允许申请,不允许审核 |
|
||||
| `fin_settlement`、`fin_reconciliation` | `readonly` | 不开放通用写入 |
|
||||
| `cs_ticket` | `writable` | 当前气站用户范围 |
|
||||
|
||||
@@ -149,6 +149,8 @@ Global:
|
||||
### 4.8 财务管理
|
||||
|
||||
- 本点钱包、银行卡、支付记录、钱包流水、退款和结算结果只读。
|
||||
- `payment_order` 是统一支付尝试事实,配送点端必须通过 `business_type + business_identity` 关联燃气配送订单、商城订单或配送点自有钱包充值单,并验证业务对象属于当前配送点;不得假设支付单直接包含钱包主键。
|
||||
- 未知业务类型、业务对象缺失以及用户或工作人员钱包充值无法证明属于当前配送点,不得向配送点端返回。钱包余额支付当前记录在 `wallet_record`,不伪装为统一支付单。
|
||||
- 配送点可以创建提现申请,但不能审批、驳回或标记完成。
|
||||
- 配送点管理员可以给当前配送点钱包自主充值。
|
||||
- 充值必须填写幂等号、金额、原因和备注。
|
||||
@@ -183,7 +185,8 @@ register?gas_identity={所属气站identity}&delivery_identity={当前配送点i
|
||||
| `gasorder_contract_product` | `append_only` | 解绑使用专用动作 |
|
||||
| `gasorder_contract_revision`、`product_info` | `readonly` | 合同历史及可选气瓶 |
|
||||
| `gasorder_basic` | `append_only` | 创建及专用调度/状态动作 |
|
||||
| `wallet_basic`、`wallet_bank`、`wallet_payment`、`wallet_record`、`wallet_refund` | `readonly` | 当前配送点钱包范围 |
|
||||
| `wallet_basic`、`wallet_bank`、`wallet_record`、`payment_refund` | `readonly` | 当前配送点钱包范围 |
|
||||
| `payment_order` | `readonly` | 通过业务对象验证的当前配送点统一支付记录 |
|
||||
| `wallet_recharge`、`wallet_apply_cash` | `append_only` | 充值及提现申请 |
|
||||
| `fin_settlement` | `readonly` | 当前配送点结算结果 |
|
||||
|
||||
|
||||
74
docs/操作日志_组织支付记录数据范围修复_20260819.md
Normal file
74
docs/操作日志_组织支付记录数据范围修复_20260819.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 组织支付记录数据范围修复操作日志
|
||||
|
||||
操作时间:2026-08-19 21:16:03
|
||||
|
||||
操作类型:修改、扩展
|
||||
|
||||
影响模块:后端 API、气站管理端财务、配送点管理端财务、需求文档
|
||||
|
||||
## 操作前状态
|
||||
|
||||
- 气站与配送点支付记录沿用旧钱包子表查询,执行 `payment_order.wallet_basic_id` 关联。
|
||||
- 统一支付模型没有 `wallet_basic_id`,列表与详情请求触发 PostgreSQL `SQLSTATE 42703`。
|
||||
- 气站需求把统一支付单描述为钱包范围,配送点需求仍使用已删除的 `wallet_payment`、`wallet_refund` 名称。
|
||||
- 缺少统一支付记录的组织数据范围回归测试。
|
||||
|
||||
## 具体操作
|
||||
|
||||
1. 新增公共支付范围过滤器,分别通过燃气配送订单、商城订单和组织自有充值单验证气站或配送点归属。
|
||||
2. 未知业务类型、孤立业务对象和无法证明组织归属的充值记录按失败关闭处理。
|
||||
3. 气站与配送点支付列表、详情改为复用同一过滤器,删除对支付单钱包主键的错误依赖。
|
||||
4. 增加 PostgreSQL SQL 回归测试,覆盖三类业务、两类组织、详情边界和未知主体。
|
||||
5. 修正气站与配送点需求文档的统一支付事实、组织归属和钱包余额支付边界。
|
||||
6. 新增项目文档,记录问题根因、查询结构、兼容性和后续维护规则。
|
||||
|
||||
## 行为变化
|
||||
|
||||
- 修改前:进入气站或配送点“支付记录”页面后,查询不存在的字段并返回数据库错误。
|
||||
- 修改后:支付记录按关联业务对象验证当前组织归属,列表与详情可正常查询。
|
||||
- 修改后:跨组织、未知类型、业务对象缺失、用户或工作人员充值不会进入组织端结果集。
|
||||
- 保持不变:接口路径、分页、排序、响应结构、支付模型、支付创建、渠道回调、余额支付、退款和数据库结构。
|
||||
|
||||
## 代码变更
|
||||
|
||||
- `backend/api/internal/logic/common/payment_scope.go`
|
||||
- 新增 `ScopePaymentOrdersByOwner`,统一追加气站或配送点支付数据范围。
|
||||
- 使用三组 `EXISTS` 验证 `gasorder`、`ec_order`、`recharge` 业务对象。
|
||||
- `backend/api/internal/logic/common/payment_scope_test.go`
|
||||
- 新增气站、配送点、失败关闭及详情一致性测试。
|
||||
- `backend/api/internal/logic/gas/finance.go`
|
||||
- 新增 `paymentOrderQuery`;修改 `ListPaymentOrder`、`GetPaymentOrder`。
|
||||
- `backend/api/internal/logic/delivery/finance.go`
|
||||
- 新增 `paymentOrderQuery`;修改 `ListPayment`、`GetPayment`。
|
||||
- `docs/06-气站管理系统需求.md`
|
||||
- 将支付记录从钱包直接归属修正为业务对象验证归属。
|
||||
- `docs/07-配送点管理系统需求.md`
|
||||
- 修正统一支付与退款资源名称及组织归属口径。
|
||||
- `docs/项目文档_组织支付记录数据范围修复_v1.0.md`
|
||||
- 新增完整项目说明、目录结构、兼容性和维护指南。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- `go test ./internal/logic/common ./internal/logic/gas ./internal/logic/delivery`:通过。
|
||||
- `go test ./...`:通过。
|
||||
- `go vet ./...`:通过。
|
||||
- `go build ./cmd/main/main.go`:通过。
|
||||
- 平台总后台 `pnpm contract:check`:通过,共 48 个资源。
|
||||
- 气站管理端 `pnpm contract:check`:通过,共 20 个资源。
|
||||
- 配送点管理端 `pnpm contract:check`:通过,共 18 个资源。
|
||||
- `git diff --check`:通过,无空白错误。
|
||||
- 定向搜索确认生产代码不再包含 `payment_order.wallet_basic_id` 或对应错误关联。
|
||||
|
||||
## 边界案例
|
||||
|
||||
- 归档业务对象仍可凭现存业务标识验证组织归属,不因业务状态归档丢失支付事实。
|
||||
- 未完成的燃气配送支付尝试跟随订单当前配送点;成功支付后订单不可重新分配,成功事实归属稳定。
|
||||
- 充值记录同时校验充值单主体和钱包主体,避免仅凭可伪造或漂移的单一字段放行。
|
||||
- 详情接口必须同时满足支付单标识和组织范围,不允许通过猜测 UUID 越权。
|
||||
|
||||
## 风险评估
|
||||
|
||||
- 查询新增关联子查询,依赖现有业务标识和组织字段索引;数据量增长后应关注执行计划和慢查询指标。
|
||||
- 钱包余额支付当前只形成 `wallet_record`,仍不会显示为 `payment_order`;统一建单属于后续资金链路设计,不在本次修复范围。
|
||||
- 支付创建入口尚未增加业务类型白名单;本次查询已对未知类型失败关闭,写入侧加固应独立评审。
|
||||
- 本次不涉及数据库迁移,回滚只需恢复查询代码和文档,不影响既有支付事实。
|
||||
@@ -67,7 +67,7 @@ platforms/
|
||||
|
||||
### 4.4 只读详情接口
|
||||
|
||||
新增 11 类 `GET /:identity`:合同气瓶、合同修订、候选气瓶、钱包、银行卡、支付记录、钱包流水、退款、提现、结算和对账。查询继续附加当前气站、合同、服务关系或钱包归属条件;对账资源当前无气站主体字段,详情与列表一致返回不可见。
|
||||
新增 11 类 `GET /:identity`:合同气瓶、合同修订、候选气瓶、钱包、银行卡、支付记录、钱包流水、退款、提现、结算和对账。支付记录通过关联业务对象验证当前气站归属,其他查询继续附加当前气站、合同、服务关系或钱包归属条件;对账资源当前无气站主体字段,详情与列表一致返回不可见。
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
|
||||
76
docs/项目文档_组织支付记录数据范围修复_v1.0.md
Normal file
76
docs/项目文档_组织支付记录数据范围修复_v1.0.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 组织支付记录数据范围修复项目文档 v1.0
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
- 项目名称:气站与配送点支付记录数据范围修复。
|
||||
- 实施范围:`backend/api` 的气站、配送点财务只读接口及对应需求文档。
|
||||
- 修复目标:移除统一支付单对不存在钱包主键的错误依赖,恢复支付记录列表与详情查询,并保持组织数据隔离。
|
||||
- 技术栈:Go、Gin、GORM、PostgreSQL。
|
||||
- 数据库影响:不新增字段、不执行迁移、不修改既有支付事实。
|
||||
|
||||
## 2. 问题与根因
|
||||
|
||||
`payment_order` 是全项目统一支付尝试事实,通过 `business_type` 和 `business_identity` 关联具体业务。气站与配送点财务查询沿用了旧钱包支付表的通用关联方式,尝试访问不存在的 `payment_order.wallet_basic_id`,导致 PostgreSQL 返回 `SQLSTATE 42703`。
|
||||
|
||||
该错误同时影响气站与配送点的支付记录列表和详情接口。数据库结构与统一支付模型一致,不应通过补充钱包主键掩盖查询模型错误。
|
||||
|
||||
## 3. 数据范围规则
|
||||
|
||||
```text
|
||||
payment_order
|
||||
├── business_type = gasorder
|
||||
│ └── gasorder_basic.identity = business_identity
|
||||
├── business_type = ec_order
|
||||
│ └── ec_order.identity = business_identity
|
||||
└── business_type = recharge
|
||||
└── wallet_recharge_order.identity = business_identity
|
||||
└── wallet_basic.id = wallet_basic_id
|
||||
```
|
||||
|
||||
- 气站范围分别校验 `gasorder_basic.gas_basic_id`、`ec_order.gas_station_id`,或气站自有充值单及钱包归属。
|
||||
- 配送点范围分别校验 `gasorder_basic.delivery_basic_id`、`ec_order.delivery_point_id`,或配送点自有充值单及钱包归属。
|
||||
- 查询使用 `EXISTS`,同时匹配业务类型和业务标识,避免不同业务表的标识偶撞。
|
||||
- 未知业务类型、业务对象缺失及无法证明组织归属的记录按失败关闭处理。
|
||||
- 列表和详情复用同一范围过滤器;详情增加支付单 `identity` 条件,不放宽组织校验。
|
||||
- 关联业务记录归档后,只要事实仍存在且可验证归属,支付记录仍可读取。
|
||||
|
||||
## 4. 目录结构与核心文件
|
||||
|
||||
```text
|
||||
platforms/
|
||||
├── backend/api/internal/logic/
|
||||
│ ├── common/
|
||||
│ │ ├── payment_scope.go # 统一支付组织范围过滤器
|
||||
│ │ └── payment_scope_test.go # PostgreSQL 查询边界回归测试
|
||||
│ ├── gas/finance.go # 气站支付列表与详情接入
|
||||
│ └── delivery/finance.go # 配送点支付列表与详情接入
|
||||
└── docs/
|
||||
├── 06-气站管理系统需求.md
|
||||
└── 07-配送点管理系统需求.md
|
||||
```
|
||||
|
||||
`ScopePaymentOrdersByOwner` 只接受 `gas` 和 `delivery` 两种主体类型。其他类型统一追加 `1 = 0`,避免调用方参数错误导致越权。
|
||||
|
||||
## 5. 接口兼容性
|
||||
|
||||
- 保持既有 API 路径、分页参数、响应结构和排序不变。
|
||||
- 不修改 `PaymentOrder` 模型、支付创建、渠道回调、余额扣款和退款流程。
|
||||
- 钱包余额支付目前直接写入 `wallet_record`,本次不将其拼装为虚拟 `payment_order`。
|
||||
- 当前用户和工作人员充值不属于气站或配送点支付范围;未来只有组织自有充值单且钱包归属一致时才会被查询。
|
||||
|
||||
## 6. 测试与维护
|
||||
|
||||
- 回归测试覆盖燃气配送订单、商城订单、组织自有充值三类归属路径。
|
||||
- 分别断言气站和配送点字段,防止跨组织字段混用。
|
||||
- 断言未知主体失败关闭、详情保留组织边界,且 SQL 不再引用 `payment_order.wallet_basic_id`。
|
||||
- 后续新增支付业务类型时,必须先明确其组织归属链路,再扩展公共过滤器和对应测试;不得通过付款用户反推组织。
|
||||
|
||||
## 7. 已知边界
|
||||
|
||||
- 未完成的燃气配送支付尝试跟随订单当前配送点;成功支付后订单状态不允许再次分配,因此成功支付事实归属稳定。
|
||||
- 钱包余额支付统一创建 `payment_order` 属于后续资金链路设计事项,不纳入本次缺陷修复。
|
||||
- 支付创建入口的业务类型白名单属于独立安全加固事项,本次查询只负责对未知类型失败关闭。
|
||||
|
||||
## 8. 变更记录
|
||||
|
||||
- v1.0:修复气站与配送点统一支付记录错误关联钱包主键的问题,补齐组织范围测试和需求口径。
|
||||
Reference in New Issue
Block a user