feat(jd_cookie):重构订单创建逻辑并优化相关模型
- 新增 CreateOrderReq 结构体用于统一订单创建参数- 修改 CreateOrder 方法签名,使用结构体传参替代多个参数 - 更新 jd_cookie 相关枚举值,增加 JdCookieStatusUnknown 状态 - 调整 OrderInfo 和 JdOrderInfo 模型字段,增强数据一致性 -优化订单与京东订单关联逻辑,移除冗余的 CurrentOrderId 字段 - 移除 ShouldExtractCard 方法,改为内部私有方法 shouldExtractCard- 精简 Callback 方法参数,移除不必要的 userOrderId 和 amount 参数 - 修复订单历史记录中订单号关联问题,直接使用 orderId 字段查询 - 更新控制器层参数传递方式,适配新的服务层接口定义 - 调整卡密提取逻辑,去除对用户订单实体的依赖 - 完善订单状态检查机制,提高卡密提取安全性 - 优化数据库查询逻辑,减少不必要的关联查询操作
This commit is contained in:
29
.claude/commands/openspec/apply.md
Normal file
29
.claude/commands/openspec/apply.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: OpenSpec: Apply
|
||||||
|
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||||
|
category: OpenSpec
|
||||||
|
tags: [openspec, apply]
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
**Guardrails**
|
||||||
|
|
||||||
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||||
|
- Keep changes tightly scoped to the requested outcome.
|
||||||
|
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you
|
||||||
|
don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
Track these steps as TODOs and complete them one by one.
|
||||||
|
|
||||||
|
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
|
||||||
|
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
|
||||||
|
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
|
||||||
|
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
|
||||||
|
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
|
||||||
|
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
|
||||||
|
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
28
.claude/commands/openspec/archive.md
Normal file
28
.claude/commands/openspec/archive.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: OpenSpec: Archive
|
||||||
|
description: Archive a deployed OpenSpec change and update specs.
|
||||||
|
category: OpenSpec
|
||||||
|
tags: [openspec, archive]
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
**Guardrails**
|
||||||
|
|
||||||
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||||
|
- Keep changes tightly scoped to the requested outcome.
|
||||||
|
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you
|
||||||
|
don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. Identify the requested change ID (via the prompt or `openspec list`).
|
||||||
|
2. Run `openspec archive <id> --yes` to let the CLI move the change and apply spec updates without prompts (use
|
||||||
|
`--skip-specs` only for tooling-only work).
|
||||||
|
3. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
|
||||||
|
4. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
|
||||||
|
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
|
||||||
|
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
42
.claude/commands/openspec/proposal.md
Normal file
42
.claude/commands/openspec/proposal.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: OpenSpec: Proposal
|
||||||
|
description: Scaffold a new OpenSpec change and validate strictly.
|
||||||
|
category: OpenSpec
|
||||||
|
tags: [openspec, change]
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
**Guardrails**
|
||||||
|
|
||||||
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||||
|
- Keep changes tightly scoped to the requested outcome.
|
||||||
|
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you
|
||||||
|
don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||||
|
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (
|
||||||
|
e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||||
|
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under
|
||||||
|
`openspec/changes/<id>/`.
|
||||||
|
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas
|
||||||
|
with clear relationships and sequencing.
|
||||||
|
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or
|
||||||
|
demands trade-off discussion before committing to specs.
|
||||||
|
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using
|
||||||
|
`## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference
|
||||||
|
related capabilities when relevant.
|
||||||
|
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include
|
||||||
|
validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||||
|
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
|
||||||
|
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation
|
||||||
|
fails.
|
||||||
|
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
|
||||||
|
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation
|
||||||
|
realities.
|
||||||
|
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
21
AGENTS.md
Normal file
21
AGENTS.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
|
||||||
|
# OpenSpec Instructions
|
||||||
|
|
||||||
|
These instructions are for AI assistants working in this project.
|
||||||
|
|
||||||
|
Always open `@/openspec/AGENTS.md` when the request:
|
||||||
|
|
||||||
|
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||||
|
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||||
|
- Sounds ambiguous and you need the authoritative spec before coding
|
||||||
|
|
||||||
|
Use `@/openspec/AGENTS.md` to learn:
|
||||||
|
|
||||||
|
- How to create and apply change proposals
|
||||||
|
- Spec format and conventions
|
||||||
|
- Project structure and guidelines
|
||||||
|
|
||||||
|
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||||
|
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
33
CLAUDE.md
33
CLAUDE.md
@@ -1,3 +1,25 @@
|
|||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
|
||||||
|
# OpenSpec Instructions
|
||||||
|
|
||||||
|
These instructions are for AI assistants working in this project.
|
||||||
|
|
||||||
|
Always open `@/openspec/AGENTS.md` when the request:
|
||||||
|
|
||||||
|
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||||
|
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||||
|
- Sounds ambiguous and you need the authoritative spec before coding
|
||||||
|
|
||||||
|
Use `@/openspec/AGENTS.md` to learn:
|
||||||
|
|
||||||
|
- How to create and apply change proposals
|
||||||
|
- Spec format and conventions
|
||||||
|
- Project structure and guidelines
|
||||||
|
|
||||||
|
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||||
|
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
|
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
@@ -12,6 +34,12 @@ This project uses GoFrame (GF) framework and includes a custom Makefile that del
|
|||||||
- `go run main.go` - Run the application directly
|
- `go run main.go` - Run the application directly
|
||||||
- `make up` - Update GoFrame and CLI to latest version (`gf up -a`)
|
- `make up` - Update GoFrame and CLI to latest version (`gf up -a`)
|
||||||
|
|
||||||
|
### Code Quality & Linting
|
||||||
|
|
||||||
|
- `go fmt ./...` - Format all Go files
|
||||||
|
- `go vet ./...` - Run Go vet for potential issues
|
||||||
|
- `go mod tidy` - Clean up module dependencies
|
||||||
|
|
||||||
### Code Generation (GoFrame CLI)
|
### Code Generation (GoFrame CLI)
|
||||||
|
|
||||||
- `make ctrl` - Generate controllers from API definitions (`gf gen ctrl`)
|
- `make ctrl` - Generate controllers from API definitions (`gf gen ctrl`)
|
||||||
@@ -27,12 +55,16 @@ This project uses GoFrame (GF) framework and includes a custom Makefile that del
|
|||||||
- `go test -v ./internal/logic/...` - Run tests for specific packages
|
- `go test -v ./internal/logic/...` - Run tests for specific packages
|
||||||
- `go test ./internal/logic/card_apple_account -v` - Run tests for specific module
|
- `go test ./internal/logic/card_apple_account -v` - Run tests for specific module
|
||||||
- `go test -run TestName` - Run specific test
|
- `go test -run TestName` - Run specific test
|
||||||
|
- `go test -race ./...` - Run tests with race detection
|
||||||
|
- `go test -cover ./...` - Run tests with coverage report
|
||||||
|
|
||||||
### Docker & Deployment
|
### Docker & Deployment
|
||||||
|
|
||||||
- `make image` - Build Docker image with auto-generated git-based tag
|
- `make image` - Build Docker image with auto-generated git-based tag
|
||||||
- `make image.push` - Build and push Docker image
|
- `make image.push` - Build and push Docker image
|
||||||
- `make deploy` - Deploy to kubernetes environment using kustomize
|
- `make deploy` - Deploy to kubernetes environment using kustomize
|
||||||
|
- **Environment Variables**: Set `serverName` env var for service identification in OpenTelemetry
|
||||||
|
- **Deployment Config**: Uses `manifest/deploy/kustomize/overlays/${_ENV}` for environment-specific configs
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
@@ -79,6 +111,7 @@ prefixes.
|
|||||||
### Technology Stack Details
|
### Technology Stack Details
|
||||||
|
|
||||||
- **Framework**: GoFrame v2 with heavy code generation usage
|
- **Framework**: GoFrame v2 with heavy code generation usage
|
||||||
|
- **Language**: Go 1.24+ (see go.mod)
|
||||||
- **Database**: MySQL with GoFrame ORM (DAO/DO/Entity pattern), dual database support
|
- **Database**: MySQL with GoFrame ORM (DAO/DO/Entity pattern), dual database support
|
||||||
- **Cache**: Redis for caching, sessions, rate limiting
|
- **Cache**: Redis for caching, sessions, rate limiting
|
||||||
- **Tracing**: OpenTelemetry with custom headers (`x-service-token`)
|
- **Tracing**: OpenTelemetry with custom headers (`x-service-token`)
|
||||||
|
|||||||
@@ -23,20 +23,20 @@ gf gen ctrl
|
|||||||
|
|
||||||
**修复前后对比**:
|
**修复前后对比**:
|
||||||
|
|
||||||
| 生成的接口方法 | 原控制器方法 | 修复后控制器方法 | 状态 |
|
| 生成的接口方法 | 原控制器方法 | 修复后控制器方法 | 状态 |
|
||||||
|----------------|--------------|------------------|------|
|
|------------------|----------------------|------------------|-------|
|
||||||
| `CreateAccount` | `CreateAccount` | `CreateAccount` | ✅ 匹配 |
|
| `CreateAccount` | `CreateAccount` | `CreateAccount` | ✅ 匹配 |
|
||||||
| `BatchCreate` | `BatchCreateAccount` | `BatchCreate` | ✅ 已修复 |
|
| `BatchCreate` | `BatchCreateAccount` | `BatchCreate` | ✅ 已修复 |
|
||||||
| `ListAccount` | `ListAccount` | `ListAccount` | ✅ 匹配 |
|
| `ListAccount` | `ListAccount` | `ListAccount` | ✅ 匹配 |
|
||||||
| `UpdateAccount` | `UpdateAccount` | `UpdateAccount` | ✅ 匹配 |
|
| `UpdateAccount` | `UpdateAccount` | `UpdateAccount` | ✅ 匹配 |
|
||||||
| `DeleteAccount` | `DeleteAccount` | `DeleteAccount` | ✅ 匹配 |
|
| `DeleteAccount` | `DeleteAccount` | `DeleteAccount` | ✅ 匹配 |
|
||||||
| `BatchCheck` | `BatchCheckAccount` | `BatchCheck` | ✅ 已修复 |
|
| `BatchCheck` | `BatchCheckAccount` | `BatchCheck` | ✅ 已修复 |
|
||||||
| `CreateOrder` | `CreateOrder` | `CreateOrder` | ✅ 匹配 |
|
| `CreateOrder` | `CreateOrder` | `CreateOrder` | ✅ 匹配 |
|
||||||
| `GetPaymentUrl` | `GetPaymentUrl` | `GetPaymentUrl` | ✅ 匹配 |
|
| `GetPaymentUrl` | `GetPaymentUrl` | `GetPaymentUrl` | ✅ 匹配 |
|
||||||
| `GetOrderStatus` | `GetOrderStatus` | `GetOrderStatus` | ✅ 匹配 |
|
| `GetOrderStatus` | `GetOrderStatus` | `GetOrderStatus` | ✅ 匹配 |
|
||||||
| `ListOrder` | `ListOrder` | `ListOrder` | ✅ 匹配 |
|
| `ListOrder` | `ListOrder` | `ListOrder` | ✅ 匹配 |
|
||||||
| `CookieHistory` | `GetCookieHistory` | `CookieHistory` | ✅ 已修复 |
|
| `CookieHistory` | `GetCookieHistory` | `CookieHistory` | ✅ 已修复 |
|
||||||
| `OrderHistory` | `GetOrderHistory` | `OrderHistory` | ✅ 已修复 |
|
| `OrderHistory` | `GetOrderHistory` | `OrderHistory` | ✅ 已修复 |
|
||||||
|
|
||||||
### 4. 验证结果
|
### 4. 验证结果
|
||||||
|
|
||||||
|
|||||||
@@ -90,13 +90,17 @@ type GetJdOrderRes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OrderInfo struct {
|
type OrderInfo struct {
|
||||||
|
Id int64 `json:"id" dc:"主键ID"`
|
||||||
OrderId string `json:"orderId" dc:"订单号"`
|
OrderId string `json:"orderId" dc:"订单号"`
|
||||||
|
UserOrderId string `json:"userOrderId" dc:"用户订单号"`
|
||||||
Amount float64 `json:"amount" dc:"订单金额"`
|
Amount float64 `json:"amount" dc:"订单金额"`
|
||||||
Category string `json:"category" dc:"商品品类"`
|
Category string `json:"category" dc:"商品品类"`
|
||||||
JdOrderId string `json:"jdOrderId" dc:"关联的京东订单号"`
|
JdOrderId string `json:"jdOrderId" dc:"关联的京东订单号"`
|
||||||
Status consts.OrderStatus `json:"status" dc:"状态:1待支付 2已支付 3已过期 4已取消"`
|
Status consts.OrderStatus `json:"status" dc:"状态:1待支付 2已支付 3已过期 4已取消"`
|
||||||
LastRequest *gtime.Time `json:"lastRequestAt" dc:"最后请求时间"`
|
LastRequest *gtime.Time `json:"lastRequestAt" dc:"最后请求时间"`
|
||||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
|
||||||
|
DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JdOrderInfo struct {
|
type JdOrderInfo struct {
|
||||||
@@ -111,7 +115,7 @@ type JdOrderInfo struct {
|
|||||||
WxPayUrl string `json:"wxPayUrl" dc:"微信支付链接"`
|
WxPayUrl string `json:"wxPayUrl" dc:"微信支付链接"`
|
||||||
WxPayExpireAt *gtime.Time `json:"wxPayExpireAt" dc:"微信支付链接过期时间"`
|
WxPayExpireAt *gtime.Time `json:"wxPayExpireAt" dc:"微信支付链接过期时间"`
|
||||||
OrderExpireAt *gtime.Time `json:"orderExpireAt" dc:"订单过期时间(默认24小时)"`
|
OrderExpireAt *gtime.Time `json:"orderExpireAt" dc:"订单过期时间(默认24小时)"`
|
||||||
CurrentOrderId int64 `json:"currentOrderId" dc:"当前关联的订单ID"`
|
OrderId string `json:"orderId" dc:"关联的用户订单号"`
|
||||||
PaidAt *gtime.Time `json:"paidAt" dc:"支付完成时间"`
|
PaidAt *gtime.Time `json:"paidAt" dc:"支付完成时间"`
|
||||||
CardNo string `json:"cardNo" dc:"卡号"`
|
CardNo string `json:"cardNo" dc:"卡号"`
|
||||||
CardPassword string `json:"cardPassword" dc:"卡密"`
|
CardPassword string `json:"cardPassword" dc:"卡密"`
|
||||||
@@ -119,7 +123,6 @@ type JdOrderInfo struct {
|
|||||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
|
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
|
||||||
DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"`
|
DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"`
|
||||||
OrderId string `json:"orderId" dc:"关联的用户订单号"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListOrderReq Order List Query Request
|
// ListOrderReq Order List Query Request
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -19,6 +19,7 @@ var JdCookieStatusText = map[JdCookieStatus]string{
|
|||||||
JdCookieStatusNormal: "正常",
|
JdCookieStatusNormal: "正常",
|
||||||
JdCookieStatusSuspend: "暂停",
|
JdCookieStatusSuspend: "暂停",
|
||||||
JdCookieStatusExpired: "失效",
|
JdCookieStatusExpired: "失效",
|
||||||
|
JdCookieStatusUnknown: "未知",
|
||||||
}
|
}
|
||||||
|
|
||||||
// JdOrderStatus 京东订单状态枚举
|
// JdOrderStatus 京东订单状态枚举
|
||||||
@@ -93,6 +94,7 @@ const (
|
|||||||
JdOrderChangeTypeExpire JdOrderChangeType = "expire" // 过期
|
JdOrderChangeTypeExpire JdOrderChangeType = "expire" // 过期
|
||||||
JdOrderChangeTypeInvalid JdOrderChangeType = "invalid" // 失效(新增)
|
JdOrderChangeTypeInvalid JdOrderChangeType = "invalid" // 失效(新增)
|
||||||
JdOrderChangeTypeSend JdOrderChangeType = "send" // 发货
|
JdOrderChangeTypeSend JdOrderChangeType = "send" // 发货
|
||||||
|
JdOrderChangeTypeReplace JdOrderChangeType = "replace" // 换绑
|
||||||
)
|
)
|
||||||
|
|
||||||
// JdOrderChangeTypeText 京东订单变更类型文本映射
|
// JdOrderChangeTypeText 京东订单变更类型文本映射
|
||||||
@@ -104,6 +106,7 @@ var JdOrderChangeTypeText = map[JdOrderChangeType]string{
|
|||||||
JdOrderChangeTypeExpire: "过期",
|
JdOrderChangeTypeExpire: "过期",
|
||||||
JdOrderChangeTypeInvalid: "失效",
|
JdOrderChangeTypeInvalid: "失效",
|
||||||
JdOrderChangeTypeSend: "发货",
|
JdOrderChangeTypeSend: "发货",
|
||||||
|
JdOrderChangeTypeReplace: "换绑",
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrderChangeType 订单变更类型
|
// OrderChangeType 订单变更类型
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package jd_cookie
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"kami/api/jd_cookie/v1"
|
"kami/api/jd_cookie/v1"
|
||||||
|
"kami/internal/model"
|
||||||
"kami/internal/service"
|
"kami/internal/service"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/errors/gcode"
|
"github.com/gogf/gf/v2/errors/gcode"
|
||||||
@@ -11,7 +12,12 @@ import (
|
|||||||
|
|
||||||
// CreateOrder 创建订单
|
// CreateOrder 创建订单
|
||||||
func (c *ControllerV1) CreateOrder(ctx context.Context, req *v1.CreateOrderReq) (res *v1.CreateOrderRes, err error) {
|
func (c *ControllerV1) CreateOrder(ctx context.Context, req *v1.CreateOrderReq) (res *v1.CreateOrderRes, err error) {
|
||||||
result, err := service.JdCookie().CreateOrder(ctx, req.UserOrderId, req.Amount, req.Category)
|
createOrderReq := &model.CreateOrderReq{
|
||||||
|
UserOrderId: req.UserOrderId,
|
||||||
|
Amount: req.Amount,
|
||||||
|
Category: req.Category,
|
||||||
|
}
|
||||||
|
result, err := service.JdCookie().CreateOrder(ctx, createOrderReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.WrapCode(gcode.CodeInternalError, err, "创建订单失败")
|
return nil, gerror.WrapCode(gcode.CodeInternalError, err, "创建订单失败")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type V1JdCookieJdOrderColumns struct {
|
|||||||
WxPayUrl string // 微信支付链接
|
WxPayUrl string // 微信支付链接
|
||||||
WxPayExpireAt string // 微信支付链接过期时间
|
WxPayExpireAt string // 微信支付链接过期时间
|
||||||
OrderExpireAt string // 订单过期时间(默认24小时)
|
OrderExpireAt string // 订单过期时间(默认24小时)
|
||||||
CurrentOrderId string // 当前关联的订单ID
|
OrderId string // 关联的用户订单号
|
||||||
PaidAt string // 支付完成时间
|
PaidAt string // 支付完成时间
|
||||||
CardNo string // 卡号
|
CardNo string // 卡号
|
||||||
CardPassword string // 卡密
|
CardPassword string // 卡密
|
||||||
@@ -55,7 +55,7 @@ var v1JdCookieJdOrderColumns = V1JdCookieJdOrderColumns{
|
|||||||
WxPayUrl: "wx_pay_url",
|
WxPayUrl: "wx_pay_url",
|
||||||
WxPayExpireAt: "wx_pay_expire_at",
|
WxPayExpireAt: "wx_pay_expire_at",
|
||||||
OrderExpireAt: "order_expire_at",
|
OrderExpireAt: "order_expire_at",
|
||||||
CurrentOrderId: "current_order_id",
|
OrderId: "order_id",
|
||||||
PaidAt: "paid_at",
|
PaidAt: "paid_at",
|
||||||
CardNo: "card_no",
|
CardNo: "card_no",
|
||||||
CardPassword: "card_password",
|
CardPassword: "card_password",
|
||||||
|
|||||||
@@ -51,12 +51,19 @@ amount := 100.0
|
|||||||
category := consts.ProductCategoryApple
|
category := consts.ProductCategoryApple
|
||||||
|
|
||||||
// 调用创建订单
|
// 调用创建订单
|
||||||
wxPayUrl, expireTime, jdOrderId, err := service.CreateOrder(ctx, orderId, amount, category)
|
createOrderReq := &model.CreateOrderReq{
|
||||||
|
UserOrderId: orderId,
|
||||||
|
Amount: amount,
|
||||||
|
Category: category,
|
||||||
|
}
|
||||||
|
result, err := service.CreateOrder(ctx, createOrderReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 所有 Cookie 都失败,返回错误
|
// 所有 Cookie 都失败,返回错误
|
||||||
// 错误信息会包含:已尝试的 Cookie 数量
|
// 错误信息会包含:已尝试的 Cookie 数量
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
wxPayUrl := result.WxPayUrl
|
||||||
|
jdOrderId := result.JdOrderId
|
||||||
|
|
||||||
// 成功返回支付链接
|
// 成功返回支付链接
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -15,18 +15,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CreateOrder 创建订单
|
// CreateOrder 创建订单
|
||||||
func (s *sJdCookie) CreateOrder(ctx context.Context, userOrderId string, amount float64, category consts.RedeemOrderCardCategory) (result *model.CreateOrderResult, err error) {
|
func (s *sJdCookie) CreateOrder(ctx context.Context, req *model.CreateOrderReq) (result *model.CreateOrderResult, err error) {
|
||||||
_ = s.ReleaseExpiredJdOrders(ctx)
|
_ = s.ReleaseExpiredJdOrders(ctx)
|
||||||
|
|
||||||
if userOrderId == "" {
|
if req.UserOrderId == "" {
|
||||||
return nil, gerror.New("用户订单号不能为空")
|
return nil, gerror.New("用户订单号不能为空")
|
||||||
}
|
}
|
||||||
if amount <= 0 {
|
if req.Amount <= 0 {
|
||||||
return nil, gerror.New("订单金额必须大于0")
|
return nil, gerror.New("订单金额必须大于0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户订单分布式锁,防止并发创建重复订单
|
// 获取用户订单分布式锁,防止并发创建重复订单
|
||||||
lockKey := "jd_cookie:create_order:" + userOrderId
|
lockKey := consts.OrderLockKeyPrefix + req.UserOrderId
|
||||||
lockValue, err := cache.NewCache().Lock(ctx, lockKey, time.Minute*3, time.Second*60)
|
lockValue, err := cache.NewCache().Lock(ctx, lockKey, time.Minute*3, time.Second*60)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.Wrap(err, "系统繁忙,请稍后重试")
|
return nil, gerror.Wrap(err, "系统繁忙,请稍后重试")
|
||||||
@@ -43,7 +43,7 @@ func (s *sJdCookie) CreateOrder(ctx context.Context, userOrderId string, amount
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// 在锁保护下再次检查用户订单是否已存在(双重检查)
|
// 在锁保护下再次检查用户订单是否已存在(双重检查)
|
||||||
existingOrder, err := s.getOrderByUserOrderId(ctx, userOrderId)
|
existingOrder, err := s.getOrderByUserOrderId(ctx, req.UserOrderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.Wrap(err, "检查用户订单是否存在失败")
|
return nil, gerror.Wrap(err, "检查用户订单是否存在失败")
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ func (s *sJdCookie) CreateOrder(ctx context.Context, userOrderId string, amount
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 优先尝试复用现有的京东订单
|
// 优先尝试复用现有的京东订单
|
||||||
reusableJdOrder, err := s.findReusableJdOrder(ctx, amount, category)
|
reusableJdOrder, err := s.findReusableJdOrder(ctx, req.Amount, req.Category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Warning(ctx, "查找可复用京东订单失败", err)
|
glog.Warning(ctx, "查找可复用京东订单失败", err)
|
||||||
}
|
}
|
||||||
@@ -136,8 +136,8 @@ func (s *sJdCookie) CreateOrder(ctx context.Context, userOrderId string, amount
|
|||||||
if jdOrderId == "" {
|
if jdOrderId == "" {
|
||||||
retryRes, err := s.createNewJdOrderWithRetry(ctx, &model.CreateNewJdOrderWithRetryReq{
|
retryRes, err := s.createNewJdOrderWithRetry(ctx, &model.CreateNewJdOrderWithRetryReq{
|
||||||
OrderId: internalOrderId,
|
OrderId: internalOrderId,
|
||||||
Amount: amount,
|
Amount: req.Amount,
|
||||||
Category: category,
|
Category: req.Category,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -148,19 +148,19 @@ func (s *sJdCookie) CreateOrder(ctx context.Context, userOrderId string, amount
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建订单记录
|
// 创建订单记录
|
||||||
err = s.createOrderRecord(ctx, internalOrderId, userOrderId, amount, category, jdOrderId, wxPayUrl)
|
err = s.createOrderRecord(ctx, internalOrderId, req.UserOrderId, req.Amount, req.Category, jdOrderId, wxPayUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.Wrap(err, "创建订单记录失败")
|
return nil, gerror.Wrap(err, "创建订单记录失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新京东订单的当前关联订单ID
|
// 更新京东订单的当前关联订单ID
|
||||||
_ = s.updateJdOrderCurrentOrderId(ctx, jdOrderId, internalOrderId)
|
_ = s.updateJdOrderId(ctx, jdOrderId, internalOrderId)
|
||||||
|
|
||||||
// 记录Cookie使用历史
|
// 记录Cookie使用历史
|
||||||
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
|
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
|
||||||
CookieId: cookieId,
|
CookieId: cookieId,
|
||||||
ChangeType: consts.CookieChangeTypeUse,
|
ChangeType: consts.CookieChangeTypeUse,
|
||||||
StatusBefore: consts.JdCookieStatusNormal,
|
StatusBefore: consts.JdCookieStatusUnknown,
|
||||||
StatusAfter: consts.JdCookieStatusNormal,
|
StatusAfter: consts.JdCookieStatusNormal,
|
||||||
UserOrderId: internalOrderId,
|
UserOrderId: internalOrderId,
|
||||||
FailureCount: 0,
|
FailureCount: 0,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (s *sJdCookie) GetJdOrder(ctx context.Context, jdOrderId string) (order *v1
|
|||||||
WxPayUrl: jdOrderEntity.WxPayUrl,
|
WxPayUrl: jdOrderEntity.WxPayUrl,
|
||||||
WxPayExpireAt: jdOrderEntity.WxPayExpireAt,
|
WxPayExpireAt: jdOrderEntity.WxPayExpireAt,
|
||||||
OrderExpireAt: jdOrderEntity.OrderExpireAt,
|
OrderExpireAt: jdOrderEntity.OrderExpireAt,
|
||||||
CurrentOrderId: jdOrderEntity.CurrentOrderId,
|
OrderId: jdOrderEntity.OrderId,
|
||||||
CardNo: jdOrderEntity.CardNo,
|
CardNo: jdOrderEntity.CardNo,
|
||||||
CardPassword: jdOrderEntity.CardPassword,
|
CardPassword: jdOrderEntity.CardPassword,
|
||||||
CardExtractedAt: jdOrderEntity.CardExtractedAt,
|
CardExtractedAt: jdOrderEntity.CardExtractedAt,
|
||||||
@@ -76,17 +76,6 @@ func (s *sJdCookie) GetJdOrder(ctx context.Context, jdOrderId string) (order *v1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取关联的用户订单信息
|
|
||||||
if jdOrderEntity.CurrentOrderId > 0 {
|
|
||||||
var userOrder *entity.V1JdCookieOrder
|
|
||||||
_ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().Id, jdOrderEntity.CurrentOrderId).
|
|
||||||
Scan(&userOrder)
|
|
||||||
if userOrder != nil {
|
|
||||||
order.OrderId = userOrder.OrderId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,17 +101,8 @@ func (s *sJdCookie) ListJdOrder(ctx context.Context, page, size int, status cons
|
|||||||
m = m.WhereLTE(dao.V1JdCookieJdOrder.Columns().CreatedAt, endTime)
|
m = m.WhereLTE(dao.V1JdCookieJdOrder.Columns().CreatedAt, endTime)
|
||||||
}
|
}
|
||||||
if orderId != "" {
|
if orderId != "" {
|
||||||
// 先查询对应的内部订单ID,然后筛选
|
// 直接根据order_id字段筛选
|
||||||
var userOrder *entity.V1JdCookieOrder
|
m = m.Where(dao.V1JdCookieJdOrder.Columns().OrderId, orderId)
|
||||||
err := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).
|
|
||||||
Scan(&userOrder)
|
|
||||||
if err == nil && userOrder != nil {
|
|
||||||
m = m.Where(dao.V1JdCookieJdOrder.Columns().CurrentOrderId, userOrder.Id)
|
|
||||||
} else {
|
|
||||||
// 如果找不到对应的内部订单,返回空结果
|
|
||||||
return []*v1.JdOrderInfo{}, 0, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询总数
|
// 查询总数
|
||||||
@@ -176,7 +156,7 @@ func (s *sJdCookie) ListJdOrder(ctx context.Context, page, size int, status cons
|
|||||||
WxPayUrl: jdOrderEntity.WxPayUrl,
|
WxPayUrl: jdOrderEntity.WxPayUrl,
|
||||||
WxPayExpireAt: jdOrderEntity.WxPayExpireAt,
|
WxPayExpireAt: jdOrderEntity.WxPayExpireAt,
|
||||||
OrderExpireAt: jdOrderEntity.OrderExpireAt,
|
OrderExpireAt: jdOrderEntity.OrderExpireAt,
|
||||||
CurrentOrderId: jdOrderEntity.CurrentOrderId,
|
OrderId: jdOrderEntity.OrderId,
|
||||||
PaidAt: jdOrderEntity.PaidAt,
|
PaidAt: jdOrderEntity.PaidAt,
|
||||||
CardNo: jdOrderEntity.CardNo,
|
CardNo: jdOrderEntity.CardNo,
|
||||||
CardPassword: jdOrderEntity.CardPassword,
|
CardPassword: jdOrderEntity.CardPassword,
|
||||||
@@ -198,17 +178,6 @@ func (s *sJdCookie) ListJdOrder(ctx context.Context, page, size int, status cons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取关联的用户订单信息
|
|
||||||
if jdOrderEntity.CurrentOrderId > 0 {
|
|
||||||
var userOrder *entity.V1JdCookieOrder
|
|
||||||
_ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().Id, jdOrderEntity.CurrentOrderId).
|
|
||||||
Scan(&userOrder)
|
|
||||||
if userOrder != nil {
|
|
||||||
info.OrderId = userOrder.OrderId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list = append(list, info)
|
list = append(list, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +420,7 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查订单状态是否允许提取卡密
|
// 检查订单状态是否允许提取卡密
|
||||||
if !s.ShouldExtractCard(ctx, jdOrder) {
|
if !s.shouldExtractCard(ctx, jdOrder) {
|
||||||
glog.Debug(ctx, "订单状态不允许提取卡密", g.Map{
|
glog.Debug(ctx, "订单状态不允许提取卡密", g.Map{
|
||||||
"jdOrderId": jdOrderId,
|
"jdOrderId": jdOrderId,
|
||||||
"status": jdOrder,
|
"status": jdOrder,
|
||||||
@@ -459,14 +428,6 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录卡密提取历史
|
|
||||||
var order *entity.V1JdCookieOrder
|
|
||||||
if jdOrder.CurrentOrderId > 0 {
|
|
||||||
_ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().Id, jdOrder.CurrentOrderId).
|
|
||||||
Scan(&order)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取Cookie信息
|
// 获取Cookie信息
|
||||||
cookieInfo, err := s.getCookieById(ctx, jdOrder.CookieId)
|
cookieInfo, err := s.getCookieById(ctx, jdOrder.CookieId)
|
||||||
if err != nil || cookieInfo == nil {
|
if err != nil || cookieInfo == nil {
|
||||||
@@ -484,13 +445,12 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
|||||||
if resp.IsCkFailed {
|
if resp.IsCkFailed {
|
||||||
s.handleCookieFailure(ctx, cookieInfo.CookieId, jdOrderId, true, resp.Remark)
|
s.handleCookieFailure(ctx, cookieInfo.CookieId, jdOrderId, true, resp.Remark)
|
||||||
_ = s.UpdateJdOrderStatus(ctx, jdOrderId, consts.JdOrderStatusCkFailed, "", resp.Remark)
|
_ = s.UpdateJdOrderStatus(ctx, jdOrderId, consts.JdOrderStatusCkFailed, "", resp.Remark)
|
||||||
if order != nil {
|
_, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().OrderId, jdOrder.OrderId).
|
||||||
_, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().Id, order.Id).
|
Update(do.V1JdCookieOrder{
|
||||||
Update(do.V1JdCookieOrder{
|
Status: consts.OrderStatusCkFailed,
|
||||||
Status: consts.OrderStatusCkFailed,
|
})
|
||||||
})
|
_ = s.RecordOrderHistory(ctx, jdOrder.OrderId, consts.OrderChangeTypeCkFailed, jdOrderId)
|
||||||
_ = s.RecordOrderHistory(ctx, order.OrderId, consts.OrderChangeTypeCkFailed, jdOrderId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return gerror.Wrap(err, "查询京东订单支付状态失败")
|
return gerror.Wrap(err, "查询京东订单支付状态失败")
|
||||||
@@ -507,13 +467,13 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
|||||||
PaidAt: gtime.Now(),
|
PaidAt: gtime.Now(),
|
||||||
Status: consts.JdOrderStatusPaid,
|
Status: consts.JdOrderStatusPaid,
|
||||||
})
|
})
|
||||||
if order != nil {
|
if jdOrder.OrderId != "" {
|
||||||
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypePay, order.OrderId, jdOrder.WxPayUrl, "")
|
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypePay, jdOrder.OrderId, jdOrder.WxPayUrl, "")
|
||||||
_, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().Id, order.Id).
|
_, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().OrderId, jdOrder.OrderId).
|
||||||
Update(do.V1JdCookieOrder{
|
Update(do.V1JdCookieOrder{
|
||||||
Status: consts.OrderStatusPaid,
|
Status: consts.OrderStatusPaid,
|
||||||
})
|
})
|
||||||
_ = s.RecordOrderHistory(ctx, order.OrderId, consts.OrderChangeTypePay, jdOrderId)
|
_ = s.RecordOrderHistory(ctx, jdOrder.OrderId, consts.OrderChangeTypePay, jdOrderId)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -521,13 +481,16 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
|||||||
return gerror.New("获取卡密信息为空")
|
return gerror.New("获取卡密信息为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
if order != nil && order.Status != int(consts.OrderStatusPaid) {
|
affected, _ := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
||||||
_, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().Id, order.Id).
|
Where(dao.V1JdCookieOrder.Columns().OrderId, jdOrder.OrderId).
|
||||||
Update(do.V1JdCookieOrder{
|
WhereNot(dao.V1JdCookieOrder.Columns().Status, consts.OrderStatusPaid).
|
||||||
Status: consts.OrderStatusPaid,
|
UpdateAndGetAffected(do.V1JdCookieOrder{
|
||||||
})
|
Status: consts.OrderStatusPaid,
|
||||||
_ = s.RecordOrderHistory(ctx, order.OrderId, consts.OrderChangeTypePay, jdOrderId)
|
})
|
||||||
|
if affected > 0 {
|
||||||
|
_ = s.RecordOrderHistory(ctx, jdOrder.OrderId, consts.OrderChangeTypePay, jdOrderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存卡密信息到数据库
|
// 保存卡密信息到数据库
|
||||||
_, err = dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
_, err = dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
||||||
Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).
|
Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).
|
||||||
@@ -541,39 +504,46 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
|||||||
return gerror.Wrap(err, "保存卡密信息失败")
|
return gerror.Wrap(err, "保存卡密信息失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, order.OrderId, jdOrder.WxPayUrl, "")
|
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, jdOrder.OrderId, jdOrder.WxPayUrl, "")
|
||||||
|
|
||||||
//提取成功要回调
|
//提取成功要回调
|
||||||
go s.Callback(ctx, order.OrderId, order.UserOrderId, order.Amount.InexactFloat64())
|
go s.Callback(ctx, jdOrder.OrderId)
|
||||||
|
|
||||||
glog.Info(ctx, "卡密提取成功", g.Map{
|
glog.Info(ctx, "卡密提取成功", g.Map{
|
||||||
"jdOrderId": jdOrderId,
|
"jdOrderId": jdOrderId,
|
||||||
"cardNo": resp.CardNo,
|
"cardNo": resp.CardNo,
|
||||||
"orderId": order.OrderId,
|
"orderId": jdOrder.OrderId,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback TODO:临时的回调
|
// Callback TODO:临时的回调
|
||||||
func (s *sJdCookie) Callback(ctx context.Context, orderId, userOrderId string, amount float64) {
|
func (s *sJdCookie) Callback(ctx context.Context, orderId string) {
|
||||||
|
var order *entity.V1JdCookieOrder
|
||||||
|
if err := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
||||||
|
Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).Scan(&order); err != nil || order == nil || order.Id == 0 {
|
||||||
|
glog.Error(ctx, "查询订单失败", g.Map{"orderId": orderId, "err": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var data *entity.V1OrderInfo
|
var data *entity.V1OrderInfo
|
||||||
if err := dao.V1OrderInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1OrderInfo.Columns().MerchantOrderId, userOrderId).Scan(&data); err != nil || data == nil || data.Id == 0 {
|
if err := dao.V1OrderInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1OrderInfo.Columns().BankOrderId, order.UserOrderId).Scan(&data); err != nil || data == nil || data.Id == 0 {
|
||||||
glog.Error(ctx, "查询订单失败", g.Map{"userOrderId": userOrderId, "err": err})
|
glog.Error(ctx, "查询订单失败", g.Map{"userOrderId": order.UserOrderId, "err": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response, _ := gclient.New().Get(ctx, "http://kami_gateway:12309/appleCard/notify", g.Map{
|
response, _ := gclient.New().Get(ctx, "http://kami_gateway:12309/appleCard/notify", g.Map{
|
||||||
"attach": data.BankOrderId,
|
"attach": data.BankOrderId,
|
||||||
"merchantId": orderId,
|
"merchantId": orderId,
|
||||||
"amount": amount,
|
"amount": order.Amount,
|
||||||
"status": "1",
|
"status": "1",
|
||||||
"sign": "123456",
|
"sign": "123456",
|
||||||
})
|
})
|
||||||
glog.Info(ctx, "回调成功", g.Map{"response": response.ReadAllString()})
|
glog.Info(ctx, "回调成功", g.Map{"response": response.ReadAllString()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldExtractCard 判断是否需要提取卡密
|
// shouldExtractCard 判断是否需要提取卡密
|
||||||
func (s *sJdCookie) ShouldExtractCard(ctx context.Context, jdOrder *entity.V1JdCookieJdOrder) bool {
|
func (s *sJdCookie) shouldExtractCard(ctx context.Context, jdOrder *entity.V1JdCookieJdOrder) bool {
|
||||||
if jdOrder == nil {
|
if jdOrder == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -621,17 +591,10 @@ func (s *sJdCookie) CleanupExpiredOrders(ctx context.Context) error {
|
|||||||
|
|
||||||
// 为每个过期的京东订单记录历史
|
// 为每个过期的京东订单记录历史
|
||||||
for _, jdOrder := range expiredJdOrders {
|
for _, jdOrder := range expiredJdOrders {
|
||||||
orderId := ""
|
orderId := jdOrder.OrderId
|
||||||
if jdOrder.CurrentOrderId > 0 {
|
if jdOrder.OrderId != "" {
|
||||||
var order *entity.V1JdCookieOrder
|
// 同时记录用户订单的历史
|
||||||
_ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
_ = s.RecordOrderHistory(ctx, orderId, consts.OrderChangeTypeExpire, jdOrder.JdOrderId)
|
||||||
Where(dao.V1JdCookieOrder.Columns().Id, jdOrder.CurrentOrderId).
|
|
||||||
Scan(&order)
|
|
||||||
if order != nil {
|
|
||||||
orderId = order.OrderId
|
|
||||||
// 同时记录用户订单的历史
|
|
||||||
_ = s.RecordOrderHistory(ctx, orderId, consts.OrderChangeTypeExpire, jdOrder.JdOrderId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ = s.RecordJdOrderHistory(ctx, jdOrder.JdOrderId, consts.JdOrderChangeTypeExpire, orderId, jdOrder.WxPayUrl, "")
|
_ = s.RecordJdOrderHistory(ctx, jdOrder.JdOrderId, consts.JdOrderChangeTypeExpire, orderId, jdOrder.WxPayUrl, "")
|
||||||
}
|
}
|
||||||
@@ -683,10 +646,10 @@ func (s *sJdCookie) ReleaseExpiredJdOrders(ctx context.Context) error {
|
|||||||
jdOrderModel := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
|
jdOrderModel := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
|
||||||
_, err := jdOrderModel.
|
_, err := jdOrderModel.
|
||||||
Where(dao.V1JdCookieJdOrder.Columns().Status, int(consts.JdOrderStatusPending)).
|
Where(dao.V1JdCookieJdOrder.Columns().Status, int(consts.JdOrderStatusPending)).
|
||||||
WhereNotNull(dao.V1JdCookieJdOrder.Columns().CurrentOrderId).
|
WhereNotNull(dao.V1JdCookieJdOrder.Columns().OrderId).
|
||||||
WhereLT(dao.V1JdCookieJdOrder.Columns().CreatedAt, expireTime).
|
WhereLT(dao.V1JdCookieJdOrder.Columns().CreatedAt, expireTime).
|
||||||
Update(g.Map{
|
Update(do.V1JdCookieJdOrder{
|
||||||
dao.V1JdCookieJdOrder.Columns().CurrentOrderId: nil,
|
OrderId: nil,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Error(ctx, "释放过期京东订单关联失败", err)
|
glog.Error(ctx, "释放过期京东订单关联失败", err)
|
||||||
@@ -710,17 +673,8 @@ func (s *sJdCookie) ExportJdOrder(ctx context.Context, status consts.JdOrderStat
|
|||||||
m = m.WhereLTE(dao.V1JdCookieJdOrder.Columns().CreatedAt, endTime)
|
m = m.WhereLTE(dao.V1JdCookieJdOrder.Columns().CreatedAt, endTime)
|
||||||
}
|
}
|
||||||
if orderId != "" {
|
if orderId != "" {
|
||||||
// 先查询对应的内部订单ID,然后筛选
|
// 直接根据order_id字段筛选
|
||||||
var userOrder *entity.V1JdCookieOrder
|
m = m.Where(dao.V1JdCookieJdOrder.Columns().OrderId, orderId)
|
||||||
err := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).
|
|
||||||
Scan(&userOrder)
|
|
||||||
if err == nil && userOrder != nil {
|
|
||||||
m = m.Where(dao.V1JdCookieJdOrder.Columns().CurrentOrderId, userOrder.Id)
|
|
||||||
} else {
|
|
||||||
// 如果找不到对应的内部订单,返回空Excel
|
|
||||||
return s.createEmptyExcel(ctx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询所有符合条件的订单
|
// 查询所有符合条件的订单
|
||||||
@@ -778,17 +732,7 @@ func (s *sJdCookie) ExportJdOrder(ctx context.Context, status consts.JdOrderStat
|
|||||||
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), jdOrder.JdOrderId)
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), jdOrder.JdOrderId)
|
||||||
|
|
||||||
// 用户订单号
|
// 用户订单号
|
||||||
userOrderId := ""
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), jdOrder.OrderId)
|
||||||
if jdOrder.CurrentOrderId > 0 {
|
|
||||||
var order *entity.V1JdCookieOrder
|
|
||||||
_ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().Id, jdOrder.CurrentOrderId).
|
|
||||||
Scan(&order)
|
|
||||||
if order != nil {
|
|
||||||
userOrderId = order.OrderId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), userOrderId)
|
|
||||||
|
|
||||||
// 卡号
|
// 卡号
|
||||||
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), jdOrder.CardNo)
|
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), jdOrder.CardNo)
|
||||||
|
|||||||
@@ -50,9 +50,8 @@ func (s *sJdCookie) GetPaymentUrl(ctx context.Context, orderId string) (result *
|
|||||||
CookieId: jdOrder.CookieId,
|
CookieId: jdOrder.CookieId,
|
||||||
})
|
})
|
||||||
if isCkFailed {
|
if isCkFailed {
|
||||||
s.handleCookieFailure(ctx, jdOrder.CookieId, jdOrder.JdOrderId, isCkFailed, "获取支付链接失败")
|
s.handleCookieFailure(ctx, jdOrder.CookieId, jdOrder.JdOrderId, isCkFailed, refreshErr.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if refreshErr != nil {
|
if refreshErr != nil {
|
||||||
// 刷新失败,标记旧订单为失效
|
// 刷新失败,标记旧订单为失效
|
||||||
_ = s.UpdateJdOrderStatus(ctx, jdOrder.JdOrderId, consts.JdOrderStatusExpired, jdOrder.WxPayUrl, refreshErr.Error())
|
_ = s.UpdateJdOrderStatus(ctx, jdOrder.JdOrderId, consts.JdOrderStatusExpired, jdOrder.WxPayUrl, refreshErr.Error())
|
||||||
@@ -67,7 +66,7 @@ func (s *sJdCookie) GetPaymentUrl(ctx context.Context, orderId string) (result *
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 解绑旧京东订单
|
// 解绑旧京东订单
|
||||||
_ = s.updateJdOrderCurrentOrderId(ctx, jdOrder.JdOrderId, "")
|
_ = s.updateJdOrderId(ctx, jdOrder.JdOrderId, "")
|
||||||
|
|
||||||
// 创建新的京东订单(带重试机制)
|
// 创建新的京东订单(带重试机制)
|
||||||
retryRes, createErr := s.createNewJdOrderWithRetry(ctx, &model.CreateNewJdOrderWithRetryReq{
|
retryRes, createErr := s.createNewJdOrderWithRetry(ctx, &model.CreateNewJdOrderWithRetryReq{
|
||||||
@@ -83,20 +82,22 @@ func (s *sJdCookie) GetPaymentUrl(ctx context.Context, orderId string) (result *
|
|||||||
_ = s.updateOrderJdOrderId(ctx, orderId, retryRes.JdOrderId, retryRes.WxPayUrl)
|
_ = s.updateOrderJdOrderId(ctx, orderId, retryRes.JdOrderId, retryRes.WxPayUrl)
|
||||||
|
|
||||||
// 更新京东订单的当前关联订单ID
|
// 更新京东订单的当前关联订单ID
|
||||||
_ = s.updateJdOrderCurrentOrderId(ctx, retryRes.JdOrderId, orderId)
|
_ = s.updateJdOrderId(ctx, retryRes.JdOrderId, orderId)
|
||||||
|
|
||||||
// 记录Cookie使用历史
|
|
||||||
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
|
|
||||||
CookieId: retryRes.CookieId,
|
|
||||||
ChangeType: consts.CookieChangeTypeUse,
|
|
||||||
StatusBefore: consts.JdCookieStatusNormal,
|
|
||||||
StatusAfter: consts.JdCookieStatusNormal,
|
|
||||||
UserOrderId: orderId,
|
|
||||||
FailureCount: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 记录订单重新绑定历史
|
// 记录订单重新绑定历史
|
||||||
_ = s.RecordOrderHistory(ctx, orderId, consts.OrderChangeTypeRebind, retryRes.JdOrderId)
|
go func() {
|
||||||
|
_ = s.RecordOrderHistory(ctx, orderId, consts.OrderChangeTypeRebind, retryRes.JdOrderId)
|
||||||
|
// 记录Cookie使用历史
|
||||||
|
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
|
||||||
|
CookieId: retryRes.CookieId,
|
||||||
|
ChangeType: consts.CookieChangeTypeUse,
|
||||||
|
StatusBefore: consts.JdCookieStatusNormal,
|
||||||
|
StatusAfter: consts.JdCookieStatusNormal,
|
||||||
|
UserOrderId: orderId,
|
||||||
|
FailureCount: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
// 返回新的支付信息
|
// 返回新的支付信息
|
||||||
jdOrderId = retryRes.JdOrderId
|
jdOrderId = retryRes.JdOrderId
|
||||||
@@ -133,13 +134,17 @@ func (s *sJdCookie) GetOrder(ctx context.Context, orderId string) (order *v1.Ord
|
|||||||
}
|
}
|
||||||
|
|
||||||
order = &v1.OrderInfo{
|
order = &v1.OrderInfo{
|
||||||
|
Id: orderEntity.Id,
|
||||||
OrderId: orderEntity.OrderId,
|
OrderId: orderEntity.OrderId,
|
||||||
|
UserOrderId: orderEntity.UserOrderId,
|
||||||
Amount: gconv.Float64(orderEntity.Amount),
|
Amount: gconv.Float64(orderEntity.Amount),
|
||||||
Category: orderEntity.Category,
|
Category: orderEntity.Category,
|
||||||
JdOrderId: orderEntity.JdOrderId,
|
JdOrderId: orderEntity.JdOrderId,
|
||||||
Status: consts.OrderStatus(orderEntity.Status),
|
Status: consts.OrderStatus(orderEntity.Status),
|
||||||
LastRequest: orderEntity.LastRequestAt,
|
LastRequest: orderEntity.LastRequestAt,
|
||||||
CreatedAt: orderEntity.CreatedAt,
|
CreatedAt: orderEntity.CreatedAt,
|
||||||
|
UpdatedAt: orderEntity.UpdatedAt,
|
||||||
|
DeletedAt: orderEntity.DeletedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -160,13 +165,17 @@ func (s *sJdCookie) GetOrderStatus(ctx context.Context, orderId string) (order *
|
|||||||
}
|
}
|
||||||
|
|
||||||
order = &v1.OrderInfo{
|
order = &v1.OrderInfo{
|
||||||
|
Id: orderEntity.Id,
|
||||||
OrderId: orderEntity.OrderId,
|
OrderId: orderEntity.OrderId,
|
||||||
|
UserOrderId: orderEntity.UserOrderId,
|
||||||
Amount: gconv.Float64(orderEntity.Amount),
|
Amount: gconv.Float64(orderEntity.Amount),
|
||||||
Category: orderEntity.Category,
|
Category: orderEntity.Category,
|
||||||
JdOrderId: orderEntity.JdOrderId,
|
JdOrderId: orderEntity.JdOrderId,
|
||||||
Status: consts.OrderStatus(orderEntity.Status),
|
Status: consts.OrderStatus(orderEntity.Status),
|
||||||
LastRequest: orderEntity.LastRequestAt,
|
LastRequest: orderEntity.LastRequestAt,
|
||||||
CreatedAt: orderEntity.CreatedAt,
|
CreatedAt: orderEntity.CreatedAt,
|
||||||
|
UpdatedAt: orderEntity.UpdatedAt,
|
||||||
|
DeletedAt: orderEntity.DeletedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -211,13 +220,17 @@ func (s *sJdCookie) ListOrder(ctx context.Context, page, size int, status consts
|
|||||||
list = make([]*v1.OrderInfo, 0, len(orders))
|
list = make([]*v1.OrderInfo, 0, len(orders))
|
||||||
for _, orderEntity := range orders {
|
for _, orderEntity := range orders {
|
||||||
info := &v1.OrderInfo{
|
info := &v1.OrderInfo{
|
||||||
|
Id: orderEntity.Id,
|
||||||
OrderId: orderEntity.OrderId,
|
OrderId: orderEntity.OrderId,
|
||||||
|
UserOrderId: orderEntity.UserOrderId,
|
||||||
Amount: gconv.Float64(orderEntity.Amount),
|
Amount: gconv.Float64(orderEntity.Amount),
|
||||||
Category: orderEntity.Category,
|
Category: orderEntity.Category,
|
||||||
JdOrderId: orderEntity.JdOrderId,
|
JdOrderId: orderEntity.JdOrderId,
|
||||||
Status: consts.OrderStatus(orderEntity.Status),
|
Status: consts.OrderStatus(orderEntity.Status),
|
||||||
LastRequest: orderEntity.LastRequestAt,
|
LastRequest: orderEntity.LastRequestAt,
|
||||||
CreatedAt: orderEntity.CreatedAt,
|
CreatedAt: orderEntity.CreatedAt,
|
||||||
|
UpdatedAt: orderEntity.UpdatedAt,
|
||||||
|
DeletedAt: orderEntity.DeletedAt,
|
||||||
}
|
}
|
||||||
list = append(list, info)
|
list = append(list, info)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,47 +18,47 @@ func TestShouldExtractCard(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// 测试空订单
|
// 测试空订单
|
||||||
result := s.ShouldExtractCard(ctx, nil)
|
result := s.shouldExtractCard(ctx, nil)
|
||||||
t.Assert(result, false)
|
t.Assert(result, false)
|
||||||
|
|
||||||
// 测试未支付订单
|
// 测试未支付订单
|
||||||
jdOrder := &entity.V1JdCookieJdOrder{
|
jdOrder := &entity.V1JdCookieJdOrder{
|
||||||
Status: int(consts.JdOrderStatusPending),
|
Status: int(consts.JdOrderStatusPending),
|
||||||
}
|
}
|
||||||
result = s.ShouldExtractCard(ctx, jdOrder)
|
result = s.shouldExtractCard(ctx, jdOrder)
|
||||||
t.Assert(result, true) // 待支付状态可以提取卡密
|
t.Assert(result, true) // 待支付状态可以提取卡密
|
||||||
|
|
||||||
// 测试已支付订单
|
// 测试已支付订单
|
||||||
jdOrder.Status = int(consts.JdOrderStatusPaid)
|
jdOrder.Status = int(consts.JdOrderStatusPaid)
|
||||||
result = s.ShouldExtractCard(ctx, jdOrder)
|
result = s.shouldExtractCard(ctx, jdOrder)
|
||||||
t.Assert(result, true) // 已支付状态可以提取卡密
|
t.Assert(result, true) // 已支付状态可以提取卡密
|
||||||
|
|
||||||
// 测试已支付且有支付时间的订单
|
// 测试已支付且有支付时间的订单
|
||||||
now := gtime.Now()
|
now := gtime.Now()
|
||||||
jdOrder.PaidAt = now
|
jdOrder.PaidAt = now
|
||||||
result = s.ShouldExtractCard(ctx, jdOrder)
|
result = s.shouldExtractCard(ctx, jdOrder)
|
||||||
t.Assert(result, true)
|
t.Assert(result, true)
|
||||||
|
|
||||||
// 测试已经提取过卡密的订单
|
// 测试已经提取过卡密的订单
|
||||||
jdOrder.CardNo = "1234567890"
|
jdOrder.CardNo = "1234567890"
|
||||||
result = s.ShouldExtractCard(ctx, jdOrder)
|
result = s.shouldExtractCard(ctx, jdOrder)
|
||||||
t.Assert(result, false)
|
t.Assert(result, false)
|
||||||
|
|
||||||
// 测试卡密不完整的订单(只有卡密没有卡号)
|
// 测试卡密不完整的订单(只有卡密没有卡号)
|
||||||
jdOrder.CardNo = ""
|
jdOrder.CardNo = ""
|
||||||
jdOrder.CardPassword = "password123"
|
jdOrder.CardPassword = "password123"
|
||||||
result = s.ShouldExtractCard(ctx, jdOrder)
|
result = s.shouldExtractCard(ctx, jdOrder)
|
||||||
t.Assert(result, false)
|
t.Assert(result, false)
|
||||||
|
|
||||||
// 测试其他状态(已发货)
|
// 测试其他状态(已发货)
|
||||||
jdOrder.CardPassword = ""
|
jdOrder.CardPassword = ""
|
||||||
jdOrder.Status = int(consts.JdOrderStatusSent)
|
jdOrder.Status = int(consts.JdOrderStatusSent)
|
||||||
result = s.ShouldExtractCard(ctx, jdOrder)
|
result = s.shouldExtractCard(ctx, jdOrder)
|
||||||
t.Assert(result, false)
|
t.Assert(result, false)
|
||||||
|
|
||||||
// 测试其他状态(已过期)
|
// 测试其他状态(已过期)
|
||||||
jdOrder.Status = int(consts.JdOrderStatusExpired)
|
jdOrder.Status = int(consts.JdOrderStatusExpired)
|
||||||
result = s.ShouldExtractCard(ctx, jdOrder)
|
result = s.shouldExtractCard(ctx, jdOrder)
|
||||||
t.Assert(result, false)
|
t.Assert(result, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,71 +153,27 @@ func (s *sJdCookie) updateJdOrderPaymentUrl(ctx context.Context, jdOrderId, wxPa
|
|||||||
|
|
||||||
// 记录支付链接更新历史
|
// 记录支付链接更新历史
|
||||||
if oldOrder != nil && oldOrder.WxPayUrl != wxPayUrl {
|
if oldOrder != nil && oldOrder.WxPayUrl != wxPayUrl {
|
||||||
orderId := ""
|
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, oldOrder.OrderId, wxPayUrl, "")
|
||||||
if oldOrder.CurrentOrderId > 0 {
|
|
||||||
var order *entity.V1JdCookieOrder
|
|
||||||
_ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().Id, oldOrder.CurrentOrderId).
|
|
||||||
Scan(&order)
|
|
||||||
if order != nil {
|
|
||||||
orderId = order.OrderId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, orderId, wxPayUrl, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateJdOrderCurrentOrderId 更新京东订单的当前关联订单ID
|
// updateJdOrderId 更新京东订单关联的用户订单ID
|
||||||
func (s *sJdCookie) updateJdOrderCurrentOrderId(ctx context.Context, jdOrderId, orderId string) error {
|
func (s *sJdCookie) updateJdOrderId(ctx context.Context, jdOrderId, orderId string) error {
|
||||||
m := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
|
m := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
|
||||||
|
|
||||||
// 如果orderId为空,表示解绑
|
|
||||||
if orderId == "" {
|
|
||||||
// 获取更新前的京东订单信息
|
|
||||||
var oldJdOrder *entity.V1JdCookieJdOrder
|
|
||||||
err := m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Scan(&oldJdOrder)
|
|
||||||
if err != nil || oldJdOrder == nil {
|
|
||||||
glog.Warning(ctx, "查询京东订单失败,无法记录历史", err)
|
|
||||||
// 即使查询失败也继续更新
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解绑:设置为null
|
|
||||||
_, err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Update(&do.V1JdCookieJdOrder{
|
|
||||||
CurrentOrderId: nil,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录解绑历史
|
|
||||||
if oldJdOrder != nil && oldJdOrder.CurrentOrderId > 0 {
|
|
||||||
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeUnbind, "", oldJdOrder.WxPayUrl, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找订单ID对应的内部ID
|
|
||||||
var order *entity.V1JdCookieOrder
|
|
||||||
err := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).
|
|
||||||
Scan(&order)
|
|
||||||
if err != nil || order == nil {
|
|
||||||
return gerror.New("订单不存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取更新前的京东订单信息
|
// 获取更新前的京东订单信息
|
||||||
var oldJdOrder *entity.V1JdCookieJdOrder
|
var oldJdOrder *entity.V1JdCookieJdOrder
|
||||||
err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Scan(&oldJdOrder)
|
err := m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Scan(&oldJdOrder)
|
||||||
if err != nil || oldJdOrder == nil {
|
if err != nil || oldJdOrder == nil {
|
||||||
glog.Warning(ctx, "查询京东订单失败,无法记录历史", err)
|
glog.Warning(ctx, "查询京东订单失败,无法记录历史", err)
|
||||||
// 即使查询失败也继续更新
|
// 即使查询失败也继续更新
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新订单ID
|
||||||
_, err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Update(&do.V1JdCookieJdOrder{
|
_, err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Update(&do.V1JdCookieJdOrder{
|
||||||
CurrentOrderId: order.Id,
|
OrderId: orderId,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -226,10 +182,10 @@ func (s *sJdCookie) updateJdOrderCurrentOrderId(ctx context.Context, jdOrderId,
|
|||||||
// 记录订单绑定历史
|
// 记录订单绑定历史
|
||||||
var changeType consts.JdOrderChangeType
|
var changeType consts.JdOrderChangeType
|
||||||
if oldJdOrder != nil {
|
if oldJdOrder != nil {
|
||||||
if oldJdOrder.CurrentOrderId == 0 {
|
if oldJdOrder.OrderId == "" {
|
||||||
changeType = consts.JdOrderChangeTypeBind // 首次绑定
|
changeType = consts.JdOrderChangeTypeBind // 首次绑定
|
||||||
} else if oldJdOrder.CurrentOrderId != order.Id {
|
} else if oldJdOrder.OrderId != orderId {
|
||||||
changeType = consts.JdOrderChangeTypeSend // 更换绑定
|
changeType = consts.JdOrderChangeTypeReplace // 更换绑定
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
changeType = consts.JdOrderChangeTypeBind // 首次绑定
|
changeType = consts.JdOrderChangeTypeBind // 首次绑定
|
||||||
@@ -256,12 +212,12 @@ func (s *sJdCookie) findReusableJdOrder(ctx context.Context, amount float64, cat
|
|||||||
// 查找符合条件的京东订单:
|
// 查找符合条件的京东订单:
|
||||||
// 1. 金额和品类相同
|
// 1. 金额和品类相同
|
||||||
// 2. 状态为待支付
|
// 2. 状态为待支付
|
||||||
// 3. 没有当前关联订单或者关联订单已过期
|
// 3. 没有关联订单
|
||||||
// 4. 订单未过期
|
// 4. 订单未过期
|
||||||
err = m.Where(dao.V1JdCookieJdOrder.Columns().Amount, amount).
|
err = m.Where(dao.V1JdCookieJdOrder.Columns().Amount, amount).
|
||||||
Where(dao.V1JdCookieJdOrder.Columns().Category, category).
|
Where(dao.V1JdCookieJdOrder.Columns().Category, category).
|
||||||
Where(dao.V1JdCookieJdOrder.Columns().Status, int(consts.JdOrderStatusPending)).
|
Where(dao.V1JdCookieJdOrder.Columns().Status, int(consts.JdOrderStatusPending)).
|
||||||
WhereNull(dao.V1JdCookieJdOrder.Columns().CurrentOrderId).
|
WhereNull(dao.V1JdCookieJdOrder.Columns().OrderId).
|
||||||
WhereGT(dao.V1JdCookieJdOrder.Columns().OrderExpireAt, gtime.Now()).
|
WhereGT(dao.V1JdCookieJdOrder.Columns().OrderExpireAt, gtime.Now()).
|
||||||
OrderAsc(dao.V1JdCookieJdOrder.Columns().CreatedAt).
|
OrderAsc(dao.V1JdCookieJdOrder.Columns().CreatedAt).
|
||||||
Limit(1).
|
Limit(1).
|
||||||
|
|||||||
@@ -251,18 +251,8 @@ func (s *sJdCookie) UpdateJdOrderStatus(ctx context.Context, jdOrderId string, s
|
|||||||
changeType = consts.JdOrderChangeTypeSend
|
changeType = consts.JdOrderChangeTypeSend
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前关联的订单ID
|
// 获取关联的订单ID
|
||||||
orderId := ""
|
orderId := oldOrder.OrderId
|
||||||
if oldOrder.CurrentOrderId > 0 {
|
|
||||||
// 查询订单表获取订单号
|
|
||||||
var order *entity.V1JdCookieOrder
|
|
||||||
_ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
|
|
||||||
Where(dao.V1JdCookieOrder.Columns().Id, oldOrder.CurrentOrderId).
|
|
||||||
Scan(&order)
|
|
||||||
if order != nil {
|
|
||||||
orderId = order.OrderId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payUrl := wxPayUrl
|
payUrl := wxPayUrl
|
||||||
if payUrl == "" {
|
if payUrl == "" {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type V1JdCookieJdOrder struct {
|
|||||||
WxPayUrl any // 微信支付链接
|
WxPayUrl any // 微信支付链接
|
||||||
WxPayExpireAt *gtime.Time // 微信支付链接过期时间
|
WxPayExpireAt *gtime.Time // 微信支付链接过期时间
|
||||||
OrderExpireAt *gtime.Time // 订单过期时间(默认24小时)
|
OrderExpireAt *gtime.Time // 订单过期时间(默认24小时)
|
||||||
CurrentOrderId any // 当前关联的订单ID
|
OrderId any // 关联的用户订单号
|
||||||
PaidAt *gtime.Time // 支付完成时间
|
PaidAt *gtime.Time // 支付完成时间
|
||||||
CardNo any // 卡号
|
CardNo any // 卡号
|
||||||
CardPassword any // 卡密
|
CardPassword any // 卡密
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type V1JdCookieJdOrder struct {
|
|||||||
WxPayUrl string `json:"wxPayUrl" orm:"wx_pay_url" description:"微信支付链接"`
|
WxPayUrl string `json:"wxPayUrl" orm:"wx_pay_url" description:"微信支付链接"`
|
||||||
WxPayExpireAt *gtime.Time `json:"wxPayExpireAt" orm:"wx_pay_expire_at" description:"微信支付链接过期时间"`
|
WxPayExpireAt *gtime.Time `json:"wxPayExpireAt" orm:"wx_pay_expire_at" description:"微信支付链接过期时间"`
|
||||||
OrderExpireAt *gtime.Time `json:"orderExpireAt" orm:"order_expire_at" description:"订单过期时间(默认24小时)"`
|
OrderExpireAt *gtime.Time `json:"orderExpireAt" orm:"order_expire_at" description:"订单过期时间(默认24小时)"`
|
||||||
CurrentOrderId int64 `json:"currentOrderId" orm:"current_order_id" description:"当前关联的订单ID"`
|
OrderId string `json:"orderId" orm:"order_id" description:"关联的用户订单号"`
|
||||||
PaidAt *gtime.Time `json:"paidAt" orm:"paid_at" description:"支付完成时间"`
|
PaidAt *gtime.Time `json:"paidAt" orm:"paid_at" description:"支付完成时间"`
|
||||||
CardNo string `json:"cardNo" orm:"card_no" description:"卡号"`
|
CardNo string `json:"cardNo" orm:"card_no" description:"卡号"`
|
||||||
CardPassword string `json:"cardPassword" orm:"card_password" description:"卡密"`
|
CardPassword string `json:"cardPassword" orm:"card_password" description:"卡密"`
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import "kami/internal/consts"
|
|||||||
// JD Cookie 相关模型结构体
|
// JD Cookie 相关模型结构体
|
||||||
// ====================================================================================
|
// ====================================================================================
|
||||||
|
|
||||||
|
// CreateOrderReq 创建订单请求参数
|
||||||
|
type CreateOrderReq struct {
|
||||||
|
UserOrderId string `json:"userOrderId" dc:"用户订单号"`
|
||||||
|
Amount float64 `json:"amount" dc:"订单金额"`
|
||||||
|
Category consts.RedeemOrderCardCategory `json:"category" dc:"卡券类别"`
|
||||||
|
}
|
||||||
|
|
||||||
// CreateOrderResult 创建订单返回结果
|
// CreateOrderResult 创建订单返回结果
|
||||||
type CreateOrderResult struct {
|
type CreateOrderResult struct {
|
||||||
WxPayUrl string `json:"wxPayUrl" dc:"微信支付链接"`
|
WxPayUrl string `json:"wxPayUrl" dc:"微信支付链接"`
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
v1 "kami/api/jd_cookie/v1"
|
v1 "kami/api/jd_cookie/v1"
|
||||||
"kami/internal/consts"
|
"kami/internal/consts"
|
||||||
"kami/internal/model"
|
"kami/internal/model"
|
||||||
"kami/internal/model/entity"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -38,7 +37,7 @@ type (
|
|||||||
// GetJdOrderHistoryByJdOrderId 根据京东订单ID获取京东订单历史
|
// GetJdOrderHistoryByJdOrderId 根据京东订单ID获取京东订单历史
|
||||||
GetJdOrderHistoryByJdOrderId(ctx context.Context, jdOrderId string, page int, size int) (list []*v1.JdOrderHistoryInfo, total int, err error)
|
GetJdOrderHistoryByJdOrderId(ctx context.Context, jdOrderId string, page int, size int) (list []*v1.JdOrderHistoryInfo, total int, err error)
|
||||||
// CreateOrder 创建订单
|
// CreateOrder 创建订单
|
||||||
CreateOrder(ctx context.Context, userOrderId string, amount float64, category consts.RedeemOrderCardCategory) (result *model.CreateOrderResult, err error)
|
CreateOrder(ctx context.Context, req *model.CreateOrderReq) (result *model.CreateOrderResult, err error)
|
||||||
// GetJdOrder 获取单个京东订单
|
// GetJdOrder 获取单个京东订单
|
||||||
GetJdOrder(ctx context.Context, jdOrderId string) (order *v1.JdOrderInfo, err error)
|
GetJdOrder(ctx context.Context, jdOrderId string) (order *v1.JdOrderInfo, err error)
|
||||||
// ListJdOrder 京东订单列表查询
|
// ListJdOrder 京东订单列表查询
|
||||||
@@ -50,9 +49,7 @@ type (
|
|||||||
// ExtractCardInfo 提取卡密信息
|
// ExtractCardInfo 提取卡密信息
|
||||||
ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
ExtractCardInfo(ctx context.Context, jdOrderId string) error
|
||||||
// Callback TODO:临时的回调
|
// Callback TODO:临时的回调
|
||||||
Callback(ctx context.Context, orderId string, userOrderId string, amount float64)
|
Callback(ctx context.Context, orderId string)
|
||||||
// ShouldExtractCard 判断是否需要提取卡密
|
|
||||||
ShouldExtractCard(ctx context.Context, jdOrder *entity.V1JdCookieJdOrder) bool
|
|
||||||
// CleanupExpiredOrders 清理过期订单(定时任务)
|
// CleanupExpiredOrders 清理过期订单(定时任务)
|
||||||
CleanupExpiredOrders(ctx context.Context) error
|
CleanupExpiredOrders(ctx context.Context) error
|
||||||
// ReleaseExpiredJdOrders 释放过期京东订单的关联(使其可以被复用)
|
// ReleaseExpiredJdOrders 释放过期京东订单的关联(使其可以被复用)
|
||||||
|
|||||||
507
openspec/AGENTS.md
Normal file
507
openspec/AGENTS.md
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
# OpenSpec Instructions
|
||||||
|
|
||||||
|
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||||
|
|
||||||
|
## TL;DR Quick Checklist
|
||||||
|
|
||||||
|
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||||
|
- Decide scope: new capability vs modify existing capability
|
||||||
|
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||||
|
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||||
|
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per
|
||||||
|
requirement
|
||||||
|
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||||
|
- Request approval: Do not start implementation until proposal is approved
|
||||||
|
|
||||||
|
## Three-Stage Workflow
|
||||||
|
|
||||||
|
### Stage 1: Creating Changes
|
||||||
|
|
||||||
|
Create proposal when you need to:
|
||||||
|
|
||||||
|
- Add features or functionality
|
||||||
|
- Make breaking changes (API, schema)
|
||||||
|
- Change architecture or patterns
|
||||||
|
- Optimize performance (changes behavior)
|
||||||
|
- Update security patterns
|
||||||
|
|
||||||
|
Triggers (examples):
|
||||||
|
|
||||||
|
- "Help me create a change proposal"
|
||||||
|
- "Help me plan a change"
|
||||||
|
- "Help me create a proposal"
|
||||||
|
- "I want to create a spec proposal"
|
||||||
|
- "I want to create a spec"
|
||||||
|
|
||||||
|
Loose matching guidance:
|
||||||
|
|
||||||
|
- Contains one of: `proposal`, `change`, `spec`
|
||||||
|
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||||
|
|
||||||
|
Skip proposal for:
|
||||||
|
|
||||||
|
- Bug fixes (restore intended behavior)
|
||||||
|
- Typos, formatting, comments
|
||||||
|
- Dependency updates (non-breaking)
|
||||||
|
- Configuration changes
|
||||||
|
- Tests for existing behavior
|
||||||
|
|
||||||
|
**Workflow**
|
||||||
|
|
||||||
|
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||||
|
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas
|
||||||
|
under `openspec/changes/<id>/`.
|
||||||
|
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||||
|
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||||
|
|
||||||
|
### Stage 2: Implementing Changes
|
||||||
|
|
||||||
|
Track these steps as TODOs and complete them one by one.
|
||||||
|
|
||||||
|
1. **Read proposal.md** - Understand what's being built
|
||||||
|
2. **Read design.md** (if exists) - Review technical decisions
|
||||||
|
3. **Read tasks.md** - Get implementation checklist
|
||||||
|
4. **Implement tasks sequentially** - Complete in order
|
||||||
|
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||||
|
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||||
|
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||||
|
|
||||||
|
### Stage 3: Archiving Changes
|
||||||
|
|
||||||
|
After deployment, create separate PR to:
|
||||||
|
|
||||||
|
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||||
|
- Update `specs/` if capabilities changed
|
||||||
|
- Use `openspec archive [change] --skip-specs --yes` for tooling-only changes
|
||||||
|
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||||
|
|
||||||
|
## Before Any Task
|
||||||
|
|
||||||
|
**Context Checklist:**
|
||||||
|
|
||||||
|
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||||
|
- [ ] Check pending changes in `changes/` for conflicts
|
||||||
|
- [ ] Read `openspec/project.md` for conventions
|
||||||
|
- [ ] Run `openspec list` to see active changes
|
||||||
|
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||||
|
|
||||||
|
**Before Creating Specs:**
|
||||||
|
|
||||||
|
- Always check if capability already exists
|
||||||
|
- Prefer modifying existing specs over creating duplicates
|
||||||
|
- Use `openspec show [spec]` to review current state
|
||||||
|
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||||
|
|
||||||
|
### Search Guidance
|
||||||
|
|
||||||
|
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||||
|
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||||
|
- Show details:
|
||||||
|
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||||
|
- Change: `openspec show <change-id> --json --deltas-only`
|
||||||
|
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### CLI Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Essential commands
|
||||||
|
openspec list # List active changes
|
||||||
|
openspec list --specs # List specifications
|
||||||
|
openspec show [item] # Display change or spec
|
||||||
|
openspec diff [change] # Show spec differences
|
||||||
|
openspec validate [item] # Validate changes or specs
|
||||||
|
openspec archive [change] [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||||
|
|
||||||
|
# Project management
|
||||||
|
openspec init [path] # Initialize OpenSpec
|
||||||
|
openspec update [path] # Update instruction files
|
||||||
|
|
||||||
|
# Interactive mode
|
||||||
|
openspec show # Prompts for selection
|
||||||
|
openspec validate # Bulk validation mode
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
openspec show [change] --json --deltas-only
|
||||||
|
openspec validate [change] --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Flags
|
||||||
|
|
||||||
|
- `--json` - Machine-readable output
|
||||||
|
- `--type change|spec` - Disambiguate items
|
||||||
|
- `--strict` - Comprehensive validation
|
||||||
|
- `--no-interactive` - Disable prompts
|
||||||
|
- `--skip-specs` - Archive without spec updates
|
||||||
|
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
openspec/
|
||||||
|
├── project.md # Project conventions
|
||||||
|
├── specs/ # Current truth - what IS built
|
||||||
|
│ └── [capability]/ # Single focused capability
|
||||||
|
│ ├── spec.md # Requirements and scenarios
|
||||||
|
│ └── design.md # Technical patterns
|
||||||
|
├── changes/ # Proposals - what SHOULD change
|
||||||
|
│ ├── [change-name]/
|
||||||
|
│ │ ├── proposal.md # Why, what, impact
|
||||||
|
│ │ ├── tasks.md # Implementation checklist
|
||||||
|
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||||
|
│ │ └── specs/ # Delta changes
|
||||||
|
│ │ └── [capability]/
|
||||||
|
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||||
|
│ └── archive/ # Completed changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Change Proposals
|
||||||
|
|
||||||
|
### Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
New request?
|
||||||
|
├─ Bug fix restoring spec behavior? → Fix directly
|
||||||
|
├─ Typo/format/comment? → Fix directly
|
||||||
|
├─ New feature/capability? → Create proposal
|
||||||
|
├─ Breaking change? → Create proposal
|
||||||
|
├─ Architecture change? → Create proposal
|
||||||
|
└─ Unclear? → Create proposal (safer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proposal Structure
|
||||||
|
|
||||||
|
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||||
|
|
||||||
|
2. **Write proposal.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Why
|
||||||
|
[1-2 sentences on problem/opportunity]
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- [Bullet list of changes]
|
||||||
|
- [Mark breaking changes with **BREAKING**]
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected specs: [list capabilities]
|
||||||
|
- Affected code: [key files/systems]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: New Feature
|
||||||
|
The system SHALL provide...
|
||||||
|
|
||||||
|
#### Scenario: Success case
|
||||||
|
- **WHEN** user performs action
|
||||||
|
- **THEN** expected result
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
### Requirement: Existing Feature
|
||||||
|
[Complete modified requirement]
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
### Requirement: Old Feature
|
||||||
|
**Reason**: [Why removing]
|
||||||
|
**Migration**: [How to handle]
|
||||||
|
```
|
||||||
|
|
||||||
|
If multiple capabilities are affected, create multiple delta files under
|
||||||
|
`changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||||
|
|
||||||
|
4. **Create tasks.md:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 1. Implementation
|
||||||
|
- [ ] 1.1 Create database schema
|
||||||
|
- [ ] 1.2 Implement API endpoint
|
||||||
|
- [ ] 1.3 Add frontend component
|
||||||
|
- [ ] 1.4 Write tests
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create design.md when needed:**
|
||||||
|
Create `design.md` if any of the following apply; otherwise omit it:
|
||||||
|
|
||||||
|
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||||
|
- New external dependency or significant data model changes
|
||||||
|
- Security, performance, or migration complexity
|
||||||
|
- Ambiguity that benefits from technical decisions before coding
|
||||||
|
|
||||||
|
Minimal `design.md` skeleton:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Context
|
||||||
|
[Background, constraints, stakeholders]
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
- Goals: [...]
|
||||||
|
- Non-Goals: [...]
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Decision: [What and why]
|
||||||
|
- Alternatives considered: [Options + rationale]
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
- [Risk] → Mitigation
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
[Steps, rollback]
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
- [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spec File Format
|
||||||
|
|
||||||
|
### Critical: Scenario Formatting
|
||||||
|
|
||||||
|
**CORRECT** (use #### headers):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
#### Scenario: User login success
|
||||||
|
- **WHEN** valid credentials provided
|
||||||
|
- **THEN** return JWT token
|
||||||
|
```
|
||||||
|
|
||||||
|
**WRONG** (don't use bullets or bold):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- **Scenario: User login** ❌
|
||||||
|
**Scenario**: User login ❌
|
||||||
|
### Scenario: User login ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
Every requirement MUST have at least one scenario.
|
||||||
|
|
||||||
|
### Requirement Wording
|
||||||
|
|
||||||
|
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||||
|
|
||||||
|
### Delta Operations
|
||||||
|
|
||||||
|
- `## ADDED Requirements` - New capabilities
|
||||||
|
- `## MODIFIED Requirements` - Changed behavior
|
||||||
|
- `## REMOVED Requirements` - Deprecated features
|
||||||
|
- `## RENAMED Requirements` - Name changes
|
||||||
|
|
||||||
|
Headers matched with `trim(header)` - whitespace ignored.
|
||||||
|
|
||||||
|
#### When to use ADDED vs MODIFIED
|
||||||
|
|
||||||
|
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the
|
||||||
|
change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing
|
||||||
|
requirement.
|
||||||
|
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full,
|
||||||
|
updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you
|
||||||
|
provide here; partial deltas will drop previous details.
|
||||||
|
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content)
|
||||||
|
referencing the new name.
|
||||||
|
|
||||||
|
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at
|
||||||
|
archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||||
|
|
||||||
|
Authoring a MODIFIED requirement correctly:
|
||||||
|
|
||||||
|
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||||
|
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||||
|
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||||
|
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||||
|
|
||||||
|
Example for RENAMED:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## RENAMED Requirements
|
||||||
|
- FROM: `### Requirement: Login`
|
||||||
|
- TO: `### Requirement: User Authentication`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
**"Change must have at least one delta"**
|
||||||
|
|
||||||
|
- Check `changes/[name]/specs/` exists with .md files
|
||||||
|
- Verify files have operation prefixes (## ADDED Requirements)
|
||||||
|
|
||||||
|
**"Requirement must have at least one scenario"**
|
||||||
|
|
||||||
|
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||||
|
- Don't use bullet points or bold for scenario headers
|
||||||
|
|
||||||
|
**Silent scenario parsing failures**
|
||||||
|
|
||||||
|
- Exact format required: `#### Scenario: Name`
|
||||||
|
- Debug with: `openspec show [change] --json --deltas-only`
|
||||||
|
|
||||||
|
### Validation Tips
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Always use strict mode for comprehensive checks
|
||||||
|
openspec validate [change] --strict
|
||||||
|
|
||||||
|
# Debug delta parsing
|
||||||
|
openspec show [change] --json | jq '.deltas'
|
||||||
|
|
||||||
|
# Check specific requirement
|
||||||
|
openspec show [spec] --json -r 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Happy Path Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Explore current state
|
||||||
|
openspec spec list --long
|
||||||
|
openspec list
|
||||||
|
# Optional full-text search:
|
||||||
|
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||||
|
# rg -n "^#|Requirement:" openspec/changes
|
||||||
|
|
||||||
|
# 2) Choose change id and scaffold
|
||||||
|
CHANGE=add-two-factor-auth
|
||||||
|
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||||
|
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||||
|
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||||
|
|
||||||
|
# 3) Add deltas (example)
|
||||||
|
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: Two-Factor Authentication
|
||||||
|
Users MUST provide a second factor during login.
|
||||||
|
|
||||||
|
#### Scenario: OTP required
|
||||||
|
- **WHEN** valid credentials are provided
|
||||||
|
- **THEN** an OTP challenge is required
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4) Validate
|
||||||
|
openspec validate $CHANGE --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Capability Example
|
||||||
|
|
||||||
|
```
|
||||||
|
openspec/changes/add-2fa-notify/
|
||||||
|
├── proposal.md
|
||||||
|
├── tasks.md
|
||||||
|
└── specs/
|
||||||
|
├── auth/
|
||||||
|
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||||
|
└── notifications/
|
||||||
|
└── spec.md # ADDED: OTP email notification
|
||||||
|
```
|
||||||
|
|
||||||
|
auth/spec.md
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: Two-Factor Authentication
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
notifications/spec.md
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: OTP Email Notification
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Simplicity First
|
||||||
|
|
||||||
|
- Default to <100 lines of new code
|
||||||
|
- Single-file implementations until proven insufficient
|
||||||
|
- Avoid frameworks without clear justification
|
||||||
|
- Choose boring, proven patterns
|
||||||
|
|
||||||
|
### Complexity Triggers
|
||||||
|
|
||||||
|
Only add complexity with:
|
||||||
|
|
||||||
|
- Performance data showing current solution too slow
|
||||||
|
- Concrete scale requirements (>1000 users, >100MB data)
|
||||||
|
- Multiple proven use cases requiring abstraction
|
||||||
|
|
||||||
|
### Clear References
|
||||||
|
|
||||||
|
- Use `file.ts:42` format for code locations
|
||||||
|
- Reference specs as `specs/auth/spec.md`
|
||||||
|
- Link related changes and PRs
|
||||||
|
|
||||||
|
### Capability Naming
|
||||||
|
|
||||||
|
- Use verb-noun: `user-auth`, `payment-capture`
|
||||||
|
- Single purpose per capability
|
||||||
|
- 10-minute understandability rule
|
||||||
|
- Split if description needs "AND"
|
||||||
|
|
||||||
|
### Change ID Naming
|
||||||
|
|
||||||
|
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||||
|
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||||
|
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||||
|
|
||||||
|
## Tool Selection Guide
|
||||||
|
|
||||||
|
| Task | Tool | Why |
|
||||||
|
|-----------------------|------|--------------------------|
|
||||||
|
| Find files by pattern | Glob | Fast pattern matching |
|
||||||
|
| Search code content | Grep | Optimized regex search |
|
||||||
|
| Read specific files | Read | Direct file access |
|
||||||
|
| Explore unknown scope | Task | Multi-step investigation |
|
||||||
|
|
||||||
|
## Error Recovery
|
||||||
|
|
||||||
|
### Change Conflicts
|
||||||
|
|
||||||
|
1. Run `openspec list` to see active changes
|
||||||
|
2. Check for overlapping specs
|
||||||
|
3. Coordinate with change owners
|
||||||
|
4. Consider combining proposals
|
||||||
|
|
||||||
|
### Validation Failures
|
||||||
|
|
||||||
|
1. Run with `--strict` flag
|
||||||
|
2. Check JSON output for details
|
||||||
|
3. Verify spec file format
|
||||||
|
4. Ensure scenarios properly formatted
|
||||||
|
|
||||||
|
### Missing Context
|
||||||
|
|
||||||
|
1. Read project.md first
|
||||||
|
2. Check related specs
|
||||||
|
3. Review recent archives
|
||||||
|
4. Ask for clarification
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Stage Indicators
|
||||||
|
|
||||||
|
- `changes/` - Proposed, not yet built
|
||||||
|
- `specs/` - Built and deployed
|
||||||
|
- `archive/` - Completed changes
|
||||||
|
|
||||||
|
### File Purposes
|
||||||
|
|
||||||
|
- `proposal.md` - Why and what
|
||||||
|
- `tasks.md` - Implementation steps
|
||||||
|
- `design.md` - Technical decisions
|
||||||
|
- `spec.md` - Requirements and behavior
|
||||||
|
|
||||||
|
### CLI Essentials
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openspec list # What's in progress?
|
||||||
|
openspec show [item] # View details
|
||||||
|
openspec diff [change] # What's changing?
|
||||||
|
openspec validate --strict # Is it correct?
|
||||||
|
openspec archive [change] [--yes|-y] # Mark complete (add --yes for automation)
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||||
93
openspec/project.md
Normal file
93
openspec/project.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Project Context
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A comprehensive card redemption platform (卡密兑换平台) that manages various types of gift cards and payment processing,
|
||||||
|
including Apple, T-Mall, JD, Walmart, and C-Trip cards. The platform handles order processing, merchant management, user
|
||||||
|
authentication, and integration with multiple external payment providers.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Framework**: GoFrame v2 with heavy code generation usage
|
||||||
|
- **Language**: Go 1.21+
|
||||||
|
- **Database**: MySQL with dual database setup (kami_v2 primary, kami legacy)
|
||||||
|
- **Cache**: Redis for caching, sessions, rate limiting
|
||||||
|
- **Authentication**: JWT with TOTP support, multi-login capability
|
||||||
|
- **Authorization**: Casbin RBAC
|
||||||
|
- **Tracing**: OpenTelemetry with custom headers (x-service-token)
|
||||||
|
- **Task Scheduling**: Cron jobs with graceful shutdown
|
||||||
|
- **Container**: Docker with Kubernetes deployment
|
||||||
|
- **API**: RESTful APIs with OpenTelemetry instrumentation
|
||||||
|
|
||||||
|
## Project Conventions
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- Go standard formatting (`gofmt`)
|
||||||
|
- Verb-led naming for change IDs (add-, update-, remove-, refactor-)
|
||||||
|
- Kebab-case for file and directory names
|
||||||
|
- Domain separation with clear business boundaries
|
||||||
|
|
||||||
|
### Architecture Patterns
|
||||||
|
|
||||||
|
- **DAO/DO/Entity pattern**: GoFrame ORM with generated data access layers
|
||||||
|
- **API-First**: Controllers generated from api/ definitions
|
||||||
|
- **Domain-Driven Design**: Each business domain has its own logic folder
|
||||||
|
- **Dual Database**: Separate connections for primary and legacy data
|
||||||
|
- **Graceful Shutdown**: Implemented for cron jobs and connection pools
|
||||||
|
- **Code Generation Workflow**: Database changes → make dao → make service → make ctrl
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
|
||||||
|
- Focused test coverage in critical business logic areas
|
||||||
|
- Unit tests for domain logic in `internal/logic/*/`
|
||||||
|
- Integration tests for external platform integrations
|
||||||
|
- Test command: `go test ./...` with verbose mode for specific packages
|
||||||
|
- Critical areas: Apple account management, T-Mall order processing, rate limiting
|
||||||
|
|
||||||
|
### Git Workflow
|
||||||
|
|
||||||
|
- **Main branch**: `develop` (primary development branch)
|
||||||
|
- **Feature branches**: Created from develop for new features
|
||||||
|
- **Commit format**: Conventional commits with type(scope): description
|
||||||
|
- **Code generation**: Automated through Makefile targets
|
||||||
|
- **Deployment**: Docker-based with auto-generated git-based tags
|
||||||
|
|
||||||
|
## Domain Context
|
||||||
|
|
||||||
|
### Business Domains
|
||||||
|
|
||||||
|
- **Card Platform Management**: Apple, T-Mall, JD, Walmart, C-Trip, and generic redemption cards
|
||||||
|
- **Order Processing**: Complete lifecycle with callbacks, status tracking, summaries
|
||||||
|
- **Merchant Management**: Configurations, deployments, hidden settings, steal rules
|
||||||
|
- **Channel & Road Management**: Business routing with road pools and entrance management
|
||||||
|
- **User Management**: Authentication, roles, authorization
|
||||||
|
- **Payment Processing**: Payment methods, deductions, statistics
|
||||||
|
- **JDCookie Management**: Cookie rotation, order processing, account management
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
|
||||||
|
- **Steal Rules**: Automated card acquisition logic
|
||||||
|
- **Road Pools**: Business routing management
|
||||||
|
- **Cookie Rotation**: JD account management strategy
|
||||||
|
- **Order Callbacks**: Asynchronous payment status notifications
|
||||||
|
- **Rate Limiting**: Redis-based access control
|
||||||
|
|
||||||
|
## Important Constraints
|
||||||
|
|
||||||
|
- **Security**: Built-in rate limiting, IP/device restrictions, authentication middleware
|
||||||
|
- **Performance**: Optimized for high-volume card redemption operations
|
||||||
|
- **Scalability**: Designed for multi-tenant architecture with merchant isolation
|
||||||
|
- **Compliance**: Payment processing industry standards
|
||||||
|
- **Reliability**: Graceful shutdown, error handling, monitoring integration
|
||||||
|
|
||||||
|
## External Dependencies
|
||||||
|
|
||||||
|
- **T-Mall**: OAuth gateway integration with eco.taobao.com
|
||||||
|
- **JD**: JD API integration with cookie management
|
||||||
|
- **Walmart**: Walmart API integration
|
||||||
|
- **C-Trip**: Travel platform API integration
|
||||||
|
- **Agiso**: App authentication service
|
||||||
|
- **Redis**: Caching, sessions, rate limiting
|
||||||
|
- **MySQL**: Primary and legacy data storage
|
||||||
|
- **OpenTelemetry**: Distributed tracing and monitoring
|
||||||
Reference in New Issue
Block a user