更新
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m35s

This commit is contained in:
2026-08-13 12:31:47 +08:00
parent fcfa347005
commit e134552ec5
13 changed files with 458 additions and 188 deletions

View File

@@ -39,6 +39,7 @@
- 当前 Bug 与兼容行为按实际结果记录,禁止在基线任务中顺手修复。
- 不修改既有迁移;新 Schema 变化使用新的成对迁移。
- 不访问生产服务、真实支付渠道或外部审批系统进行自动验证。
- 生产环境为 systemd 管理的手工二进制发布,和仓库 Docker/CI 测试环境不同;生产发布、迁移与回滚事实见 [`docs/deployment/production-runbook.md`](docs/deployment/production-runbook.md)。Agent 不连接生产主机或数据库,生产操作由维护者执行并提供结果。
- 不把密钥、Token、证书或个人敏感数据写入代码、文档和日志。
- `.lh-harness/` 仅保存本地执行证据,不是事实源且不得纳入 Git。
- 自动化测试当前为 N/A用户决策不恢复旧测试也不写虚假测试入口。
@@ -82,7 +83,7 @@ openspec validate --all
./scripts/context-health.sh
```
启动、隔离数据库重置、smoke 与日志读取见 [`README.md`](README.md)。迁移使用 `scripts/migrate.sh` 显式隔离数据库参数
启动、隔离数据库重置、smoke 与日志读取见 [`README.md`](README.md)。迁移使用 `scripts/migrate.sh` 显式 `DB_*` 参数;生产迁移仅按生产运行说明由维护者手工执行
## 渐进披露

View File

@@ -13,7 +13,7 @@
| OpenAPI 生成 | `cmd/gendocs/main.go` | 使用占位 Handler 复用 `internal/routes.RegisterRoutesWithDoc`,生成接口文档 | `go run cmd/gendocs/main.go`;它证明路由元数据,不单独证明真实 Bootstrap 可达性 |
| 治理 CLI | `cmd/foundation-check``cmd/audit-coverage``cmd/migration-finalize``cmd/audit-retention-simulate` | 读取源码、迁移或数据库状态并输出门禁/模拟结果 | 逐命令检查输入;`audit-coverage` 当前仍依赖已删除路径,见工程约束 BLOCKED 项 |
启动、隔离数据库、smoke 与日志命令见 [`README.md`](README.md)。
启动、隔离数据库、smoke 与日志命令见 [`README.md`](README.md)。生产为独立的 systemd 手工二进制发布拓扑,不以 Docker Compose 或 CI 工作流为准;运行事实与人工发布边界见 [`docs/deployment/production-runbook.md`](docs/deployment/production-runbook.md)。
## Worker 角色

View File

@@ -25,6 +25,7 @@ import (
approvalInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/approval"
auditInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
cardObservationInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/cardobservation"
commissionDelivery "github.com/break/junhong_cmp_fiber/internal/infrastructure/commissiondelivery"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
notificationInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/notification"
@@ -370,6 +371,16 @@ func registerWeComApprovalOutboxConsumer(runtime *workerRuntime, cfg *config.Con
)
refundService.SetNotificationOutbox(outbox.NewRepository())
refundService.SetLifecycleAudit(auditWriter)
if err := runtime.outboxConsumers.Register(commissionDelivery.EventRefundCommissionDeduct, commissionDelivery.NewRefundConsumer(refundService.ProcessCommissionDeduction, refundService.ProcessAssetPostProcessing)); err != nil {
appLogger.Fatal("注册退款佣金回扣 Outbox 消费者失败", zap.Error(err))
}
if err := runtime.outboxConsumers.Register(commissionDelivery.EventRefundAssetProcess, commissionDelivery.NewRefundConsumer(refundService.ProcessCommissionDeduction, refundService.ProcessAssetPostProcessing)); err != nil {
appLogger.Fatal("注册退款资产后处理 Outbox 消费者失败", zap.Error(err))
}
if err := runtime.outboxConsumers.Register(commissionDelivery.EventCommissionCalculate, commissionDelivery.NewCommissionConsumer(runtime.outboxQueueClient, appLogger)); err != nil {
appLogger.Fatal("注册订单佣金计算 Outbox 消费者失败", zap.Error(err))
}
commissionDelivery.Recover(context.Background(), runtime.db, outbox.NewRepository(), 100, appLogger)
decisionDispatcher := approvalApp.NewDecisionDispatcher(
approvalInfra.NewDecisionDeliveryStore(runtime.db),
map[string]approvalApp.BusinessDecisionHandler{

View File

@@ -0,0 +1,90 @@
# 生产环境运行说明
## 元数据
- 用途:区分生产与测试运行方式,并为人工发布、迁移和回滚提供事实入口。
- 适用范围:`xm-iot.cn` 的生产 API 与 Worker不适用于本地、测试环境或 Docker Compose。
- 事实源:生产维护者提供的主机与目录信息;实际 systemd Unit、线上 `.env.prod``migrate version` 输出优先于本文。
- Owner生产维护者。
- 最后核验2026-08-13基于维护者提供的信息未连接生产环境
- 更新触发Unit、目录、进程数、Worker 角色、发布顺序、迁移方式或回滚方式变化。
- 验证由维护者在服务器执行本文列出的只读核对命令并回填结果Agent 不连接生产环境。
## 与测试环境的边界
生产环境不是仓库中的 Docker Compose 测试部署:
| 项目 | 生产环境 | 仓库 Compose/工作流 |
| --- | --- | --- |
| 平台 | Ubuntu 24.04 x86_64 | 测试环境自动部署 |
| 发布 | 本地交叉编译后手工上传二进制 | Gitea 构建、镜像与 Docker Compose |
| 进程管理 | systemd | Docker Compose |
| 运行单元 | 1 个 API、1 个调度 Worker、3 个执行 Worker | 不作为生产拓扑依据 |
| 配置 | 每个运行目录的 `.env.prod` | Compose `environment` |
| 回滚 | 覆盖为部署前备份的二进制,必要时恢复数据库备份 | 不适用 |
不得从 `.gitea/workflows/deploy.yaml``docker-compose.prod.yml` 或其中的测试配置推断生产地址、凭据、发布步骤或运行拓扑。
## 已确认运行布局
- 主机Ubuntu 24.04.4 LTS`x86_64`
- API 服务 `junhong-cmp-api.service`:工作目录与程序分别为 `/opt/junhong_cmp/api``/opt/junhong_cmp/api/api`,通过同目录 `.env.prod` 加载环境变量;旧二进制备份在 `releases/`
- 调度 Worker 服务 `junhong-cmp-worker.service`:工作目录与程序分别为 `/opt/junhong_cmp/worker``/opt/junhong_cmp/worker/worker`,通过根目录 `.env.prod` 加载环境变量。
- 三个执行 Worker 服务 `junhong-cmp-worker-1.service``junhong-cmp-worker-2.service``junhong-cmp-worker-3.service`:共享 `/opt/junhong_cmp/worker/worker` 这一份二进制;工作目录和 `EnvironmentFile` 分别是 `worker-1/``worker-2/``worker-3/` 下的 `.env.prod`。因此发布只替换根目录的 `worker`,不复制三份二进制。
- 五个 Unit 都以 `root``Restart=always``RestartSec=3` 运行,标准输出和错误写入 journald。
- Worker 拓扑:调度实例已确认 `JUNHONG_WORKER_ROLE=leader``JUNHONG_WORKER_INSTANCE_NAME=leader`;三个执行实例均为 `JUNHONG_WORKER_ROLE=consumer`。当前三个执行实例的 `JUNHONG_WORKER_INSTANCE_NAME` 都是 `worker-1`,会混淆日志与 Outbox Relay 实例标识;七月发布前须分别修正为 `worker-1``worker-2``worker-3`
- 域名:`xm-iot.cn`API 域名:`cmp-api.xm-iot.cn`
- 企微回调由自建企微中台 `wecom.xm-iot.cn` 配置;支付和运营商回调由各官方平台配置。
## 构建与上传
维护者当前在本地构建:
```bash
GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o ./build/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o ./build/worker ./cmd/worker
```
本次迭代更新只上传 API 与 Worker 二进制;配置变化由维护者直接修改各运行目录的 `.env.prod`。生产迁移文件必须随本次发布上传到 API 目录下的 `migrations/`,供 `migrate` 记录和执行。生产服务器当前尚未安装 `migrate`;安装路径和版本待首次迁移前确认。
## 配置规则
- 二进制只从嵌入默认配置和 `JUNHONG_` 环境变量读取;`.env.prod` 必须由 systemd Unit 显式加载,或由 Unit 启动脚本 `source` 后启动。需要以 Unit 内容核实实际方式。
- API 与 Worker 共享数据库、Redis、日志、JWT、对象存储、Gateway、短信、支付等基础配置只记录键名不将实际凭据写入仓库文档。
- 七月新增的 API/Worker 共用配置:`JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED``JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED``JUNHONG_WECOM_BASE_URL``JUNHONG_WECOM_TIMEOUT`
- 七月新增的 Worker 配置:`JUNHONG_WORKER_ROLE``JUNHONG_WORKER_INSTANCE_NAME``JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED`
- 首发要求:退款人工入口、线下充值人工确认、企微审批、运营商实名回调、微信/支付宝在线充值均按维护者决定启用;审计物理清理保持关闭。
- 企微应用凭据由后台配置写入数据库明文字段;这不是启动环境变量。本文不记录其值。
## 人工发布与迁移
已确认顺序:备份二进制和数据库 → 上传二进制及迁移文件 → 停止 systemd 服务 → 执行迁移 → 启动服务。
生产当前迁移版本已于 2026-08-13 由维护者在 API 目录验证为 `140`(非 dirty。历史迁移已归档生产从 `140` 向后执行根目录 `migrations/``141+` 迁移;七月分支最终目标为 `206`。根目录不存在 `173` 号迁移,这是正常编号空档。执行前先以显式 `DB_*` 参数运行:
```bash
DB_HOST=<生产主机> DB_PORT=<端口> DB_USER=<用户> \
DB_PASSWORD='<密码>' DB_NAME=<库名> DB_SSLMODE=<模式> \
./scripts/migrate.sh version
DB_HOST=<生产主机> DB_PORT=<端口> DB_USER=<用户> \
DB_PASSWORD='<密码>' DB_NAME=<库名> DB_SSLMODE=<模式> \
./scripts/migrate.sh up
```
迁移失败时不启动新二进制;按失败迁移的事务状态决定处理,必要时恢复已确认可用的数据库备份。启动失败时覆盖回部署前备份的二进制,再恢复数据库备份(如迁移已改变数据库)。
### 锁的含义与发布影响
`000171` 会对 `tb_agent_wallet``000178` 会对 `tb_iot_card` 使用 PostgreSQL `ACCESS EXCLUSIVE` 锁。该锁执行期间会阻塞该表的读写及其他 DDL直到迁移事务提交或回滚若有未结束业务查询/事务,它也会等待。因此必须在 API 和全部 Worker 停止后执行,并在迁移前检查没有长事务。锁持续时间取决于表数据量、索引创建和等待中的旧事务;不能从仓库估算具体秒数。
## 待维护者确认的最小信息
1. `migrate` 的安装路径、版本和迁移文件上传命令;确认 `140` 到首个根目录迁移版本之间是否存在待补的迁移文件。
2. 数据库恢复的准确命令、恢复前提,以及发布前新建备份的执行人。
3. 已停止服务后的锁前检查命令/结果(至少确认无长事务),以及可接受维护窗口。
4. 企微中台转发到 API 的最终回调路径;支付与运营商平台配置的回调 URL 清单(可脱敏域名外路径)。
## 已确认数据库备份
每日凌晨 02:00 自动备份 `junhong_cmp_prod`:数据库运行在 Docker 容器 `postgres` 中,备份脚本执行 `pg_dump -Fc -Z 6`,写入 `/data/backups/postgresql/<库名>_<时间>.dump`,同时生成 MD5 文件并以 `pg_restore --list` 校验结构;保留 30 天。发布前仍须人工新建一次备份并确认校验通过,不能只依赖凌晨的最近备份。恢复命令待维护者实际演练或确认后补充。

View File

@@ -93,6 +93,19 @@
- **最后验证日期**2026-08-07
- **更新触发条件**:数据访问基础设施变化
## ENG-DB-002
- **状态**:生效
- **适用范围**Agent 对项目数据库的诊断、核对与只读查询。
- **规则**MUST 使用 dbhub MCP测试/本地库使用 `mcp__dbhub__execute_sql_main`,正式库使用 `mcp__dbhub__execute_sql_pro_main`MUST NOT 通过 `psql`、连接串、环境变量或其他命令行客户端直连数据库。
- **理由**dbhub 提供受控只读访问,避免命令历史、环境凭证和目标库选择漂移。
- **最小正例**:调用 `mcp__dbhub__execute_sql_main` 查询订单与佣金记录。
- **最小反例**`source .env && psql ...`
- **机械检查/人工原因**:审查 Agent 执行记录中的数据库访问工具;仓库业务 Go 代码不受本条约束,仍遵守 ENG-DB-001。
- **例外条件**维护者明确提供的、需执行写入或迁移的人工操作按生产运行说明执行Agent 不代执行。
- **Owner**:基础设施负责人
- **最后验证日期**2026-08-13
- **更新触发条件**dbhub MCP 名称、访问范围或数据库运维边界变化
## ENG-MIG-001
- **状态**:生效
- **适用范围**`migrations/` 当前根目录

View File

@@ -0,0 +1,18 @@
# reliable-order-commission-dispatch 验证记录
2026-08-13
```text
GOCACHE=/private/tmp/junhong-go-cache go build ./cmd/api ./cmd/worker
exit=0
openspec validate --all
Totals: 17 passed, 0 failed (17 items)
gofmt -d internal/infrastructure/commissiondelivery/event.go internal/service/order/service.go internal/service/refund/approval_decision.go internal/service/refund/service.go internal/task/auto_purchase.go cmd/worker/main.go
exit=0无输出
```
静态可复现核验:`rg` 确认所有已支付订单路径在事务内调用 `AppendCommissionCalculate`,退款审批路径写入两个稳定 Outbox 事件;`go func` 与直接 `commission:calculate` 入队均不再位于订单、退款和自动购包路径。补偿扫描按状态分页,缺失事件创建、失败事件复位为待投递,并记录已补发、无需补发和失败计数。
`./scripts/context-health.sh` 当前返回非零:仓库既有 `.scratch/` 目录仍在,输出为“禁止目录或文件仍存在:.scratch”。

View File

@@ -0,0 +1,187 @@
// Package commissiondelivery 提供订单佣金与退款后处理的可靠 Outbox 事件。
package commissiondelivery
import (
"context"
"strconv"
"time"
"github.com/bytedance/sonic"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/outboxid"
)
const (
EventCommissionCalculate = "order.commission.calculate.requested"
EventRefundCommissionDeduct = "refund.commission.deduct.requested"
EventRefundAssetProcess = "refund.asset.process.requested"
PayloadVersionV1 = 1
)
type Payload struct {
OrderID uint `json:"order_id"`
RefundID uint `json:"refund_id,omitempty"`
}
func AppendCommissionCalculate(ctx context.Context, tx *gorm.DB, repository *outbox.Repository, orderID uint) error {
return appendEvent(ctx, tx, repository, EventCommissionCalculate, "order", orderID, Payload{OrderID: orderID})
}
func AppendRefundCommissionDeduct(ctx context.Context, tx *gorm.DB, repository *outbox.Repository, refundID, orderID uint) error {
return appendEvent(ctx, tx, repository, EventRefundCommissionDeduct, "refund", refundID, Payload{OrderID: orderID, RefundID: refundID})
}
func AppendRefundAssetProcess(ctx context.Context, tx *gorm.DB, repository *outbox.Repository, refundID, orderID uint) error {
return appendEvent(ctx, tx, repository, EventRefundAssetProcess, "refund", refundID, Payload{OrderID: orderID, RefundID: refundID})
}
func appendEvent(ctx context.Context, tx *gorm.DB, repository *outbox.Repository, eventType, aggregate string, id uint, payload Payload) error {
if repository == nil {
return gorm.ErrInvalidDB
}
value := strconv.FormatUint(uint64(id), 10)
_, err := repository.AppendIdempotent(ctx, tx, outbox.Envelope{
EventID: outboxid.Stable(eventType+":", value), EventType: eventType, PayloadVersion: PayloadVersionV1,
AggregateType: aggregate, AggregateID: value, ResourceType: aggregate, ResourceID: value,
BusinessKey: eventType + ":" + value, Payload: payload,
})
return err
}
type CommissionConsumer struct {
client outbox.TaskEnqueuer
logger *zap.Logger
}
func NewCommissionConsumer(client outbox.TaskEnqueuer, logger *zap.Logger) *CommissionConsumer {
return &CommissionConsumer{client: client, logger: logger}
}
func (c *CommissionConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
var payload Payload
if err := sonic.Unmarshal(envelope.Payload, &payload); err != nil {
return outbox.Permanent(err)
}
if envelope.EventType != EventCommissionCalculate || envelope.PayloadVersion != PayloadVersionV1 || payload.OrderID == 0 {
return outbox.Permanent(gorm.ErrInvalidData)
}
if err := c.client.EnqueueTask(ctx, constants.TaskTypeCommission, map[string]any{"order_id": payload.OrderID, "request_id": envelope.RequestID, "correlation_id": envelope.CorrelationID, "parent_event_id": envelope.EventID}, asynq.Queue(constants.QueueForTaskType(constants.TaskTypeCommission))); err != nil {
return err
}
c.logger.Info("佣金计算 Outbox 已投递", zap.Uint("order_id", payload.OrderID), zap.String("event_id", envelope.EventID), zap.String("correlation_id", envelope.CorrelationID))
return nil
}
type RefundConsumer struct {
commission func(context.Context, uint) error
asset func(context.Context, uint) error
}
func NewRefundConsumer(commission func(context.Context, uint) error, asset func(context.Context, uint) error) *RefundConsumer {
return &RefundConsumer{commission: commission, asset: asset}
}
func (c *RefundConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
var payload Payload
if err := sonic.Unmarshal(envelope.Payload, &payload); err != nil {
return outbox.Permanent(err)
}
if payload.RefundID == 0 || payload.OrderID == 0 || envelope.PayloadVersion != PayloadVersionV1 {
return outbox.Permanent(gorm.ErrInvalidData)
}
ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: envelope.CorrelationID, ParentEventID: envelope.EventID})
switch envelope.EventType {
case EventRefundCommissionDeduct:
return c.commission(ctx, payload.RefundID)
case EventRefundAssetProcess:
return c.asset(ctx, payload.RefundID)
default:
return outbox.Permanent(gorm.ErrInvalidData)
}
}
// RecoveryStats 是一次有界补偿扫描的可观察结果。
type RecoveryStats struct{ Resent, Unchanged, Failed int }
// Recover 扫描遗留订单与退款,并用稳定事件 ID 恢复投递事实。
func Recover(ctx context.Context, db *gorm.DB, repository *outbox.Repository, limit int, logger *zap.Logger) {
if limit <= 0 {
limit = 100
}
orderStats := RecoveryStats{}
refundStats := RecoveryStats{}
var orders []model.Order
if err := db.WithContext(ctx).Where("payment_status = ? AND commission_status = ?", model.PaymentStatusPaid, model.CommissionStatusPending).Order("id ASC").Limit(limit).Find(&orders).Error; err != nil {
logger.Warn("扫描待计算订单失败", zap.Error(err))
} else {
for _, order := range orders {
recoverOne(ctx, db, repository, EventCommissionCalculate, order.ID, order.ID, &orderStats, logger)
}
}
var refunds []model.RefundRequest
if err := db.WithContext(ctx).Where("status = ? AND (commission_deducted = ? OR asset_reset = ?)", model.RefundStatusApproved, false, false).Order("id ASC").Limit(limit).Find(&refunds).Error; err != nil {
logger.Warn("扫描退款后处理失败", zap.Error(err))
} else {
for _, refund := range refunds {
if !refund.CommissionDeducted {
recoverOne(ctx, db, repository, EventRefundCommissionDeduct, refund.ID, refund.OrderID, &refundStats, logger)
}
if !refund.AssetReset {
recoverOne(ctx, db, repository, EventRefundAssetProcess, refund.ID, refund.OrderID, &refundStats, logger)
}
}
}
logger.Info("佣金与退款补偿扫描完成",
zap.Int("订单已补发", orderStats.Resent), zap.Int("订单无需补发", orderStats.Unchanged), zap.Int("订单失败", orderStats.Failed),
zap.Int("退款已补发", refundStats.Resent), zap.Int("退款无需补发", refundStats.Unchanged), zap.Int("退款失败", refundStats.Failed))
}
func recoverOne(ctx context.Context, db *gorm.DB, repository *outbox.Repository, eventType string, aggregateID, orderID uint, stats *RecoveryStats, logger *zap.Logger) {
eventID := outboxid.Stable(eventType+":", strconv.FormatUint(uint64(aggregateID), 10))
var event model.OutboxEvent
err := db.WithContext(ctx).Where("event_id = ?", eventID).First(&event).Error
if err == nil {
if event.Status != constants.OutboxStatusFailed {
stats.Unchanged++
return
}
result := db.WithContext(ctx).Model(&model.OutboxEvent{}).Where("id = ? AND status = ?", event.ID, constants.OutboxStatusFailed).Updates(map[string]any{
"status": constants.OutboxStatusPending, "retry_count": 0, "next_attempt_at": time.Now().UTC(),
"last_error_code": "", "last_error_summary": "", "updated_at": time.Now().UTC(),
})
if result.Error == nil && result.RowsAffected == 1 {
stats.Resent++
return
}
if result.Error == nil {
stats.Unchanged++
return
}
stats.Failed++
logger.Warn("恢复失败 Outbox 事件失败", zap.String("event_id", eventID), zap.Error(result.Error))
return
}
if err != gorm.ErrRecordNotFound {
stats.Failed++
logger.Warn("查询补偿 Outbox 事件失败", zap.String("event_id", eventID), zap.Error(err))
return
}
err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
switch eventType {
case EventCommissionCalculate:
return AppendCommissionCalculate(ctx, tx, repository, aggregateID)
case EventRefundCommissionDeduct:
return AppendRefundCommissionDeduct(ctx, tx, repository, aggregateID, orderID)
default:
return AppendRefundAssetProcess(ctx, tx, repository, aggregateID, orderID)
}
})
if err != nil {
stats.Failed++
logger.Warn("创建补偿 Outbox 事件失败", zap.String("event_id", eventID), zap.Error(err))
return
}
stats.Resent++
}

View File

@@ -14,7 +14,9 @@ import (
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/commissiondelivery"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
@@ -510,9 +512,6 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
if !containsGift {
s.enqueueCommissionCalculation(ctx, order.ID)
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(ctx, order, items), nil
@@ -808,7 +807,6 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(ctx, order, items), nil
@@ -1191,6 +1189,9 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
if err := commissiondelivery.AppendCommissionCalculate(ctx, tx, outbox.NewRepository(), order.ID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入佣金计算 Outbox 事件失败")
}
if err := s.appendOrderAudit(ctx, tx, constants.AuditActionOrderCreated, "创建并使用钱包支付订单", order, nil, orderStateData(order)); err != nil {
return err
}
@@ -1209,9 +1210,6 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
}
// 3. 事务外:所有已支付且适用差价佣金的订单都进入佣金计算
if order.CommissionStatus == model.CommissionStatusPending {
s.enqueueCommissionCalculation(ctx, order.ID)
}
return 0, nil
}
@@ -1232,6 +1230,9 @@ func (s *Service) createOrderWithActivation(ctx context.Context, order *model.Or
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
if err := commissiondelivery.AppendCommissionCalculate(ctx, tx, outbox.NewRepository(), order.ID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入佣金计算 Outbox 事件失败")
}
return s.appendOrderAudit(ctx, tx, constants.AuditActionOrderCreated, "创建并完成订单", order, nil, orderStateData(order))
})
}
@@ -1720,7 +1721,6 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
// 根据资源类型选择对应的钱包系统
now := time.Now()
actualPaidAmount := order.TotalAmount
shouldEnqueueCommission := false
if resourceType == "shop" {
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -1758,7 +1758,6 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
actualPaidAmountSnapshot := actualPaidAmount
order.ActualPaidAmount = &actualPaidAmountSnapshot
order.PaidAt = &now
shouldEnqueueCommission = true
agentWalletDebitAttempted = true
if err := s.debitAgentMainWalletInTx(ctx, tx, order, resourceID, order.TotalAmount, nil); err != nil {
@@ -1772,6 +1771,9 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
if err := commissiondelivery.AppendCommissionCalculate(ctx, tx, outbox.NewRepository(), order.ID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入佣金计算 Outbox 事件失败")
}
after := *order
after.PaymentStatus = model.PaymentStatusPaid
after.PaymentMethod = model.PaymentMethodWallet
@@ -1850,6 +1852,9 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
if err := commissiondelivery.AppendCommissionCalculate(ctx, tx, outbox.NewRepository(), order.ID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入佣金计算 Outbox 事件失败")
}
after := *order
after.PaymentStatus = model.PaymentStatusPaid
after.PaymentMethod = model.PaymentMethodWallet
@@ -1863,9 +1868,6 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
return err
}
if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending {
s.enqueueCommissionCalculation(ctx, orderID)
}
return nil
}
@@ -1922,7 +1924,6 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
now := time.Now()
beforeOrder := *order
shouldResumeAfterPayment := false
shouldEnqueueCommission := false
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", order.ID, model.PaymentStatusPending).
@@ -1959,11 +1960,13 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
actualPaidAmountSnapshot := actualPaidAmount
order.ActualPaidAmount = &actualPaidAmountSnapshot
shouldResumeAfterPayment = true
shouldEnqueueCommission = true
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
if err := commissiondelivery.AppendCommissionCalculate(ctx, tx, outbox.NewRepository(), order.ID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入佣金计算 Outbox 事件失败")
}
after := *order
after.PaymentStatus = model.PaymentStatusPaid
after.PaymentMethod = paymentMethod
@@ -1979,9 +1982,6 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
if shouldResumeAfterPayment {
s.tryResumeAfterPayment(ctx, order)
}
if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending {
s.enqueueCommissionCalculation(ctx, order.ID)
}
return nil
}
@@ -2010,7 +2010,6 @@ func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo str
beforePayment := *payment
beforeOrder := *order
shouldResumeAfterPayment := false
shouldEnqueueCommission := false
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
paymentUpdates := map[string]any{
"status": model.PaymentRecordStatusPaid,
@@ -2069,11 +2068,13 @@ func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo str
actualPaidAmountSnapshot := actualPaidAmount
order.ActualPaidAmount = &actualPaidAmountSnapshot
shouldResumeAfterPayment = true
shouldEnqueueCommission = true
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
if err := commissiondelivery.AppendCommissionCalculate(ctx, tx, outbox.NewRepository(), order.ID); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入佣金计算 Outbox 事件失败")
}
afterPayment := beforePayment
afterPayment.Status = model.PaymentRecordStatusPaid
afterPayment.PaidAt = &now
@@ -2097,9 +2098,6 @@ func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo str
if shouldResumeAfterPayment {
s.tryResumeAfterPayment(ctx, order)
}
if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending {
s.enqueueCommissionCalculation(ctx, order.ID)
}
return nil
}
@@ -2703,30 +2701,6 @@ func (s *Service) resolvePackageTerms(ctx context.Context, tx *gorm.DB, pkg *mod
return packagepkg.ResolveTermsFromTx(ctx, tx, pkg, sellerShopID)
}
func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint) {
if s.queueClient == nil {
s.logger.Warn("队列客户端未初始化,跳过佣金计算任务入队", zap.Uint("order_id", orderID))
return
}
linkage := auditcontext.From(ctx)
// 直接传 map由 EnqueueTask 内部统一序列化一次(传 []byte 会导致 sonic.Marshal 二次 base64 编码)
if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeCommission, map[string]any{
"order_id": orderID, "request_id": linkage.RequestID, "correlation_id": linkage.CorrelationID,
"parent_event_id": linkage.ParentEventID,
}); err != nil {
s.logger.Error("佣金计算任务入队失败",
zap.Uint("order_id", orderID),
zap.Error(err),
zap.String("task_type", constants.TaskTypeCommission))
return
}
s.logger.Info("佣金计算任务已入队",
zap.Uint("order_id", orderID),
zap.String("task_type", constants.TaskTypeCommission))
}
func (s *Service) buildOrderResponse(ctx context.Context, order *model.Order, items []*model.OrderItem) *dto.OrderResponse {
var itemResponses []*dto.OrderItemResponse
for _, item := range items {

View File

@@ -9,6 +9,8 @@ import (
"gorm.io/gorm/clause"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/commissiondelivery"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
@@ -99,6 +101,12 @@ func (s *Service) applyApprovedDecision(ctx context.Context, event approvalapp.T
if err := s.appendCompletedNotification(ctx, tx, &refund); err != nil {
return err
}
if err := commissiondelivery.AppendRefundCommissionDeduct(ctx, tx, outbox.NewRepository(), refund.ID, refund.OrderID); err != nil {
return err
}
if err := commissiondelivery.AppendRefundAssetProcess(ctx, tx, outbox.NewRepository(), refund.ID, refund.OrderID); err != nil {
return err
}
if !changed {
return nil
}
@@ -109,7 +117,7 @@ func (s *Service) applyApprovedDecision(ctx context.Context, event approvalapp.T
s.recordRefundFailure(ctx, constants.AuditActionRefundApproved, "通过退款审批失败", &refund, &order, err)
return err
}
return s.ensureApprovedPostProcessing(ctx, event.BusinessID)
return nil
}
func (s *Service) applyClosedDecision(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
@@ -159,15 +167,26 @@ func (s *Service) applyClosedDecision(ctx context.Context, event approvalapp.Ter
return err
}
func (s *Service) ensureApprovedPostProcessing(ctx context.Context, refundID uint) error {
func (s *Service) ProcessCommissionDeduction(ctx context.Context, refundID uint) error {
s.deductAllCommission(ctx, refundID)
var refund model.RefundRequest
if err := s.db.WithContext(ctx).Select("commission_deducted").First(&refund, refundID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核退款佣金回扣状态失败")
}
if !refund.CommissionDeducted {
return errors.New(errors.CodeServiceUnavailable, "退款佣金回扣尚未完成")
}
return nil
}
func (s *Service) ProcessAssetPostProcessing(ctx context.Context, refundID uint) error {
s.handleRefundAssetProcessing(ctx, refundID)
var refund model.RefundRequest
if err := s.db.WithContext(ctx).Select("commission_deducted", "asset_reset").Where("id = ?", refundID).First(&refund).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核退款后处理状态失败")
if err := s.db.WithContext(ctx).Select("asset_reset").First(&refund, refundID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "复核退款资产后处理状态失败")
}
if !refund.CommissionDeducted || !refund.AssetReset {
return errors.New(errors.CodeServiceUnavailable, "退款后处理尚未全部完成,将自动重试")
if !refund.AssetReset {
return errors.New(errors.CodeServiceUnavailable, "退款资产后处理尚未完成")
}
return nil
}

View File

@@ -19,6 +19,7 @@ import (
refundapprovalapp "github.com/break/junhong_cmp_fiber/internal/application/refundapproval"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/commissiondelivery"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
@@ -340,6 +341,12 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
if err := s.appendCompletedNotification(ctx, tx, refund); err != nil {
return err
}
if err := commissiondelivery.AppendRefundCommissionDeduct(ctx, tx, outbox.NewRepository(), refund.ID, refund.OrderID); err != nil {
return err
}
if err := commissiondelivery.AppendRefundAssetProcess(ctx, tx, outbox.NewRepository(), refund.ID, refund.OrderID); err != nil {
return err
}
return s.appendRefundAudit(ctx, tx, refund.ID, constants.AuditActionRefundApproved, "通过退款审批",
"refund:"+strconv.FormatUint(uint64(refund.ID), 10)+":approved", beforeRefund, beforeOrder, "退款已通过")
})
@@ -348,24 +355,6 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveRefundRe
return err
}
// 事务提交成功后,异步执行佣金回扣和退款后资产处理(失败不影响审批结果)
go func() {
asyncCtx := auditcontext.With(context.Background(), auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.AuditActorIDRefundCommissionPostProcessing,
ActorName: "退款佣金自动回扣任务", Source: constants.AuditSourceWorker,
CorrelationID: refund.RefundNo,
})
s.deductAllCommission(asyncCtx, id)
}()
go func() {
asyncCtx := auditcontext.With(context.Background(), auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.AuditActorIDRefundAssetPostProcessing,
ActorName: "退款资产自动后处理任务", Source: constants.AuditSourceWorker,
})
s.handleRefundAssetProcessing(asyncCtx, id)
}()
return nil
}

View File

@@ -16,6 +16,8 @@ import (
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/commissiondelivery"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
@@ -199,7 +201,6 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
}
}
var createdOrderID uint
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
wallet, walletErr := h.walletStore.GetByID(ctx, rechargeOrder.AssetWalletID)
if walletErr != nil {
@@ -225,7 +226,6 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
if err = tx.Create(order).Error; err != nil {
return err
}
createdOrderID = order.ID
for _, item := range orderItems {
item.OrderID = order.ID
@@ -277,6 +277,9 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
if resourceID == 0 {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "自动购包观测事件缺少载体")
}
if err = commissiondelivery.AppendCommissionCalculate(ctx, tx, outbox.NewRepository(), order.ID); err != nil {
return err
}
requestID := "card-observation:auto-purchase:" + strconv.FormatUint(uint64(order.ID), 10)
if err = h.observationSeriesEvents.AppendSeriesRequested(ctx, tx, cardObservationApp.SeriesRequestedEvent{
EventID: requestID, Scene: constants.CardObservationScenePackageChanged,
@@ -306,30 +309,6 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
return err
}
// 事务提交成功后触发佣金计算(不在事务内,防止任务提交后事务回滚的数据一致性问题)
if h.asynqClient != nil && createdOrderID > 0 {
linkage := auditcontext.From(ctx)
payloadBytes, marshalErr := sonic.Marshal(CommissionCalculationPayload{
OrderID: createdOrderID, RequestID: linkage.RequestID,
CorrelationID: linkage.CorrelationID, ParentEventID: linkage.ParentEventID,
})
if marshalErr != nil {
h.logger.Warn("佣金任务载荷序列化失败",
zap.Uint("order_id", createdOrderID),
zap.Error(marshalErr))
} else {
commissionTask := asynq.NewTask(constants.TaskTypeCommission, payloadBytes,
asynq.MaxRetry(3),
asynq.Queue(constants.QueueForTaskType(constants.TaskTypeCommission)),
)
if _, enqueueErr := h.asynqClient.EnqueueContext(ctx, commissionTask); enqueueErr != nil {
h.logger.Warn("自动购包后提交佣金任务失败",
zap.Uint("order_id", createdOrderID),
zap.Error(enqueueErr))
}
}
}
h.logger.Info("自动购包任务执行成功", zap.Uint("recharge_record_id", rechargeOrder.ID))
return nil
}

View File

@@ -1,91 +1,40 @@
identifier
8986032445201075309,
8986032445201075310,
8986032445201075311,
8986032445201075312,
8986032445201075313,
8986032445201075314,
8986032445201075315,
8986032445201075316,
8986032445201075317,
8986032445201075318,
8986032445201075319,
8986032445201075320,
8986032445201075321,
8986032445201075322,
8986032445201075323,
8986032445201075324,
8986032445201075325,
8986032445201075326,
8986032445201075327,
8986032445201075328,
8986032445201075329,
8986032445201075330,
8986032445201075331,
8986032445201075332,
8986032445201075333,
8986032445201075334,
8986032445201075335,
8986032445201075336,
8986032445201075337,
8986032445201075338,
8986032445201075339,
8986032445201075340,
8986032445201075341,
8986032445201075342,
8986032445201075343,
8986032445201075344,
8986032445201075345,
8986032445201075346,
8986032445201075347,
8986032445201075348,
8986032445201075349,
8986032445201075350,
8986032445201075351,
8986032445201075352,
8986032445201075353,
8986032445201075354,
8986032445201075355,
8986032445201075356,
8986032445201075357,
8986032445201075358,
8986032445201075359,
8986032445201075360,
8986032445201075361,
8986032445201075362,
8986032445201075363,
8986032445201075364,
8986032445201075365,
8986032445201075366,
8986032445201075367,
8986032445201075368,
8986032445201075369,
8986032445201075370,
8986032445201075371,
8986032445201075372,
8986032445201075373,
8986032445201075374,
8986032445201075375,
8986032445201075376,
8986032445201075377,
8986032445201075378,
8986032445201075379,
8986032445201075380,
8986032445201075381,
8986032445201075382,
8986032445201075383,
8986032445201075384,
8986032445201075385,
8986032445201075386,
8986032445201075387,
8986032445201075388,
8986032445201075389,
8986032445201075390,
8986032445201075391,
8986032445201075392,
8986032445201075393,
8986032445201075394,
8986032445201075395,
8986032445201075396,
8986032445201075397,
8986032445201075398
898604861025C0135778,
898604861025C0135779,
898604861025C0135780,
898604861025C0135781,
898604861025C0135782,
898604861025C0135783,
898604861025C0135802,
898604861025C0135803,
898604861025C0135804,
898604861025C0135805,
898604861025C0135806,
898604861025C0135807,
898604861025C0135808,
898604861025C0135811,
898604861025C0135812,
898604861025C0135831,
898604861025C0135834,
898604861025C0135835,
898604861025C0135836,
898604861025C0135837,
898604861025C0135838,
898604861025C0135839,
898604861025C0135840,
898604861025C0135841,
898604861025C0135842,
898604861025C0135843,
898604861025C0135844,
898604861025C0135845,
898604861025C0135846,
898604861025C0135847,
898604861025C0135848,
898604861025C0135849,
898604861025C0135867,
898604861025C0135868,
898604861025C0135869,
898604861025C0135870,
898604861025C0135873,
898604861025C0135018,
898604861025C0135019
1 identifier
2 8986032445201075309, 898604861025C0135778,
3 8986032445201075310, 898604861025C0135779,
4 8986032445201075311, 898604861025C0135780,
5 8986032445201075312, 898604861025C0135781,
6 8986032445201075313, 898604861025C0135782,
7 8986032445201075314, 898604861025C0135783,
8 8986032445201075315, 898604861025C0135802,
9 8986032445201075316, 898604861025C0135803,
10 8986032445201075317, 898604861025C0135804,
11 8986032445201075318, 898604861025C0135805,
12 8986032445201075319, 898604861025C0135806,
13 8986032445201075320, 898604861025C0135807,
14 8986032445201075321, 898604861025C0135808,
15 8986032445201075322, 898604861025C0135811,
16 8986032445201075323, 898604861025C0135812,
17 8986032445201075324, 898604861025C0135831,
18 8986032445201075325, 898604861025C0135834,
19 8986032445201075326, 898604861025C0135835,
20 8986032445201075327, 898604861025C0135836,
21 8986032445201075328, 898604861025C0135837,
22 8986032445201075329, 898604861025C0135838,
23 8986032445201075330, 898604861025C0135839,
24 8986032445201075331, 898604861025C0135840,
25 8986032445201075332, 898604861025C0135841,
26 8986032445201075333, 898604861025C0135842,
27 8986032445201075334, 898604861025C0135843,
28 8986032445201075335, 898604861025C0135844,
29 8986032445201075336, 898604861025C0135845,
30 8986032445201075337, 898604861025C0135846,
31 8986032445201075338, 898604861025C0135847,
32 8986032445201075339, 898604861025C0135848,
33 8986032445201075340, 898604861025C0135849,
34 8986032445201075341, 898604861025C0135867,
35 8986032445201075342, 898604861025C0135868,
36 8986032445201075343, 898604861025C0135869,
37 8986032445201075344, 898604861025C0135870,
38 8986032445201075345, 898604861025C0135873,
39 8986032445201075346, 898604861025C0135018,
40 8986032445201075347, 898604861025C0135019
8986032445201075348,
8986032445201075349,
8986032445201075350,
8986032445201075351,
8986032445201075352,
8986032445201075353,
8986032445201075354,
8986032445201075355,
8986032445201075356,
8986032445201075357,
8986032445201075358,
8986032445201075359,
8986032445201075360,
8986032445201075361,
8986032445201075362,
8986032445201075363,
8986032445201075364,
8986032445201075365,
8986032445201075366,
8986032445201075367,
8986032445201075368,
8986032445201075369,
8986032445201075370,
8986032445201075371,
8986032445201075372,
8986032445201075373,
8986032445201075374,
8986032445201075375,
8986032445201075376,
8986032445201075377,
8986032445201075378,
8986032445201075379,
8986032445201075380,
8986032445201075381,
8986032445201075382,
8986032445201075383,
8986032445201075384,
8986032445201075385,
8986032445201075386,
8986032445201075387,
8986032445201075388,
8986032445201075389,
8986032445201075390,
8986032445201075391,
8986032445201075392,
8986032445201075393,
8986032445201075394,
8986032445201075395,
8986032445201075396,
8986032445201075397,
8986032445201075398

View File

@@ -0,0 +1,40 @@
line_no,identifier,status,http_status,code,msg,order_id,order_no,total_amount
2,898604861025C0135778,成功,200,0,success,39160,ORD20260813095305432379,14800
3,898604861025C0135779,成功,200,0,success,39161,ORD20260813095306544935,14800
4,898604861025C0135780,成功,200,0,success,39162,ORD20260813095306121839,14800
5,898604861025C0135781,成功,200,0,success,39163,ORD20260813095307100605,14800
6,898604861025C0135782,成功,200,0,success,39164,ORD20260813095307817132,14800
7,898604861025C0135783,成功,200,0,success,39165,ORD20260813095308848820,14800
8,898604861025C0135802,成功,200,0,success,39166,ORD20260813095308374250,14800
9,898604861025C0135803,成功,200,0,success,39167,ORD20260813095309801863,14800
10,898604861025C0135804,成功,200,0,success,39168,ORD20260813095309062747,14800
11,898604861025C0135805,成功,200,0,success,39169,ORD20260813095310037617,14800
12,898604861025C0135806,成功,200,0,success,39170,ORD20260813095310887778,14800
13,898604861025C0135807,成功,200,0,success,39171,ORD20260813095311730198,14800
14,898604861025C0135808,成功,200,0,success,39172,ORD20260813095312181183,14800
15,898604861025C0135811,成功,200,0,success,39173,ORD20260813095312315722,14800
16,898604861025C0135812,成功,200,0,success,39174,ORD20260813095313143447,14800
17,898604861025C0135831,成功,200,0,success,39175,ORD20260813095313973171,14800
18,898604861025C0135834,成功,200,0,success,39176,ORD20260813095314991250,14800
19,898604861025C0135835,成功,200,0,success,39177,ORD20260813095315109046,14800
20,898604861025C0135836,成功,200,0,success,39178,ORD20260813095315555729,14800
21,898604861025C0135837,成功,200,0,success,39179,ORD20260813095315837735,14800
22,898604861025C0135838,成功,200,0,success,39180,ORD20260813095316476195,14800
23,898604861025C0135839,成功,200,0,success,39181,ORD20260813095316709943,14800
24,898604861025C0135840,成功,200,0,success,39182,ORD20260813095317504065,14800
25,898604861025C0135841,成功,200,0,success,39183,ORD20260813095317992778,14800
26,898604861025C0135842,成功,200,0,success,39184,ORD20260813095317943311,14800
27,898604861025C0135843,成功,200,0,success,39185,ORD20260813095318653871,14800
28,898604861025C0135844,成功,200,0,success,39186,ORD20260813095318165257,14800
29,898604861025C0135845,成功,200,0,success,39187,ORD20260813095319875764,14800
30,898604861025C0135846,成功,200,0,success,39188,ORD20260813095319101488,14800
31,898604861025C0135847,成功,200,0,success,39189,ORD20260813095319683942,14800
32,898604861025C0135848,成功,200,0,success,39190,ORD20260813095320872947,14800
33,898604861025C0135849,成功,200,0,success,39191,ORD20260813095320841981,14800
34,898604861025C0135867,成功,200,0,success,39192,ORD20260813095321280275,14800
35,898604861025C0135868,成功,200,0,success,39193,ORD20260813095321326703,14800
36,898604861025C0135869,成功,200,0,success,39194,ORD20260813095322622603,14800
37,898604861025C0135870,成功,200,0,success,39195,ORD20260813095323426463,14800
38,898604861025C0135873,成功,200,0,success,39196,ORD20260813095324009496,14800
39,898604861025C0135018,成功,200,0,success,39197,ORD20260813095324737145,14800
40,898604861025C0135019,成功,200,0,success,39198,ORD20260813095325614242,14800
1 line_no identifier status http_status code msg order_id order_no total_amount
2 2 898604861025C0135778 成功 200 0 success 39160 ORD20260813095305432379 14800
3 3 898604861025C0135779 成功 200 0 success 39161 ORD20260813095306544935 14800
4 4 898604861025C0135780 成功 200 0 success 39162 ORD20260813095306121839 14800
5 5 898604861025C0135781 成功 200 0 success 39163 ORD20260813095307100605 14800
6 6 898604861025C0135782 成功 200 0 success 39164 ORD20260813095307817132 14800
7 7 898604861025C0135783 成功 200 0 success 39165 ORD20260813095308848820 14800
8 8 898604861025C0135802 成功 200 0 success 39166 ORD20260813095308374250 14800
9 9 898604861025C0135803 成功 200 0 success 39167 ORD20260813095309801863 14800
10 10 898604861025C0135804 成功 200 0 success 39168 ORD20260813095309062747 14800
11 11 898604861025C0135805 成功 200 0 success 39169 ORD20260813095310037617 14800
12 12 898604861025C0135806 成功 200 0 success 39170 ORD20260813095310887778 14800
13 13 898604861025C0135807 成功 200 0 success 39171 ORD20260813095311730198 14800
14 14 898604861025C0135808 成功 200 0 success 39172 ORD20260813095312181183 14800
15 15 898604861025C0135811 成功 200 0 success 39173 ORD20260813095312315722 14800
16 16 898604861025C0135812 成功 200 0 success 39174 ORD20260813095313143447 14800
17 17 898604861025C0135831 成功 200 0 success 39175 ORD20260813095313973171 14800
18 18 898604861025C0135834 成功 200 0 success 39176 ORD20260813095314991250 14800
19 19 898604861025C0135835 成功 200 0 success 39177 ORD20260813095315109046 14800
20 20 898604861025C0135836 成功 200 0 success 39178 ORD20260813095315555729 14800
21 21 898604861025C0135837 成功 200 0 success 39179 ORD20260813095315837735 14800
22 22 898604861025C0135838 成功 200 0 success 39180 ORD20260813095316476195 14800
23 23 898604861025C0135839 成功 200 0 success 39181 ORD20260813095316709943 14800
24 24 898604861025C0135840 成功 200 0 success 39182 ORD20260813095317504065 14800
25 25 898604861025C0135841 成功 200 0 success 39183 ORD20260813095317992778 14800
26 26 898604861025C0135842 成功 200 0 success 39184 ORD20260813095317943311 14800
27 27 898604861025C0135843 成功 200 0 success 39185 ORD20260813095318653871 14800
28 28 898604861025C0135844 成功 200 0 success 39186 ORD20260813095318165257 14800
29 29 898604861025C0135845 成功 200 0 success 39187 ORD20260813095319875764 14800
30 30 898604861025C0135846 成功 200 0 success 39188 ORD20260813095319101488 14800
31 31 898604861025C0135847 成功 200 0 success 39189 ORD20260813095319683942 14800
32 32 898604861025C0135848 成功 200 0 success 39190 ORD20260813095320872947 14800
33 33 898604861025C0135849 成功 200 0 success 39191 ORD20260813095320841981 14800
34 34 898604861025C0135867 成功 200 0 success 39192 ORD20260813095321280275 14800
35 35 898604861025C0135868 成功 200 0 success 39193 ORD20260813095321326703 14800
36 36 898604861025C0135869 成功 200 0 success 39194 ORD20260813095322622603 14800
37 37 898604861025C0135870 成功 200 0 success 39195 ORD20260813095323426463 14800
38 38 898604861025C0135873 成功 200 0 success 39196 ORD20260813095324009496 14800
39 39 898604861025C0135018 成功 200 0 success 39197 ORD20260813095324737145 14800
40 40 898604861025C0135019 成功 200 0 success 39198 ORD20260813095325614242 14800