This commit is contained in:
@@ -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_*` 参数;生产迁移仅按生产运行说明由维护者手工执行。
|
||||
|
||||
## 渐进披露
|
||||
|
||||
|
||||
@@ -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 角色
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
90
docs/deployment/production-runbook.md
Normal file
90
docs/deployment/production-runbook.md
Normal 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 天。发布前仍须人工新建一次备份并确认校验通过,不能只依赖凌晨的最近备份。恢复命令待维护者实际演练或确认后补充。
|
||||
@@ -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/` 当前根目录
|
||||
|
||||
18
docs/verification/reliable-order-commission-dispatch.md
Normal file
18
docs/verification/reliable-order-commission-dispatch.md
Normal 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”。
|
||||
187
internal/infrastructure/commissiondelivery/event.go
Normal file
187
internal/infrastructure/commissiondelivery/event.go
Normal 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++
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user