From bc2d58753b8fe0b4f7150657bf40cfe756ce48de Mon Sep 17 00:00:00 2001 From: danial Date: Sat, 18 Oct 2025 14:13:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(jd=5Fcookie):=E9=87=8D=E6=9E=84=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=9B=B8=E5=85=B3=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CreateOrderReq 结构体用于统一订单创建参数- 修改 CreateOrder 方法签名,使用结构体传参替代多个参数 - 更新 jd_cookie 相关枚举值,增加 JdCookieStatusUnknown 状态 - 调整 OrderInfo 和 JdOrderInfo 模型字段,增强数据一致性 -优化订单与京东订单关联逻辑,移除冗余的 CurrentOrderId 字段 - 移除 ShouldExtractCard 方法,改为内部私有方法 shouldExtractCard- 精简 Callback 方法参数,移除不必要的 userOrderId 和 amount 参数 - 修复订单历史记录中订单号关联问题,直接使用 orderId 字段查询 - 更新控制器层参数传递方式,适配新的服务层接口定义 - 调整卡密提取逻辑,去除对用户订单实体的依赖 - 完善订单状态检查机制,提高卡密提取安全性 - 优化数据库查询逻辑,减少不必要的关联查询操作 --- .claude/commands/openspec/apply.md | 29 + .claude/commands/openspec/archive.md | 28 + .claude/commands/openspec/proposal.md | 42 ++ AGENTS.md | 21 + CLAUDE.md | 33 ++ CONTROLLER_GENERATION_REPORT.md | 28 +- api/jd_cookie/v1/order.go | 7 +- internal/boot/boot_enums.go | 2 +- internal/consts/jd_cookie.go | 3 + .../jd_cookie/jd_cookie_v1_create_order.go | 8 +- .../dao/internal/v_1_jd_cookie_jd_order.go | 4 +- internal/logic/jd_cookie/README.md | 9 +- internal/logic/jd_cookie/order_create.go | 22 +- internal/logic/jd_cookie/order_jd.go | 156 ++---- internal/logic/jd_cookie/order_query.go | 43 +- internal/logic/jd_cookie/order_test.go | 16 +- internal/logic/jd_cookie/order_utils.go | 66 +-- internal/logic/jd_cookie/rotation.go | 14 +- internal/model/do/v_1_jd_cookie_jd_order.go | 2 +- .../model/entity/v_1_jd_cookie_jd_order.go | 2 +- internal/model/jd_cookie.go | 7 + internal/service/jd_cookie.go | 7 +- openspec/AGENTS.md | 507 ++++++++++++++++++ openspec/project.md | 93 ++++ 24 files changed, 914 insertions(+), 235 deletions(-) create mode 100644 .claude/commands/openspec/apply.md create mode 100644 .claude/commands/openspec/archive.md create mode 100644 .claude/commands/openspec/proposal.md create mode 100644 AGENTS.md create mode 100644 openspec/AGENTS.md create mode 100644 openspec/project.md diff --git a/.claude/commands/openspec/apply.md b/.claude/commands/openspec/apply.md new file mode 100644 index 00000000..edd168fe --- /dev/null +++ b/.claude/commands/openspec/apply.md @@ -0,0 +1,29 @@ +--- +name: OpenSpec: Apply +description: Implement an approved OpenSpec change and keep tasks in sync. +category: OpenSpec +tags: [openspec, apply] +--- + + +**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//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 ` when additional context is required. + +**Reference** + +- Use `openspec show --json --deltas-only` if you need additional context from the proposal while implementing. + + diff --git a/.claude/commands/openspec/archive.md b/.claude/commands/openspec/archive.md new file mode 100644 index 00000000..c9e21b1c --- /dev/null +++ b/.claude/commands/openspec/archive.md @@ -0,0 +1,28 @@ +--- +name: OpenSpec: Archive +description: Archive a deployed OpenSpec change and update specs. +category: OpenSpec +tags: [openspec, archive] +--- + + +**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 --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 ` if anything looks off. + +**Reference** + +- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off. + + diff --git a/.claude/commands/openspec/proposal.md b/.claude/commands/openspec/proposal.md new file mode 100644 index 00000000..e2dcee36 --- /dev/null +++ b/.claude/commands/openspec/proposal.md @@ -0,0 +1,42 @@ +--- +name: OpenSpec: Proposal +description: Scaffold a new OpenSpec change and validate strictly. +category: OpenSpec +tags: [openspec, change] +--- + + +**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//`. +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//specs//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 --strict` and resolve every issue before sharing the proposal. + +**Reference** + +- Use `openspec show --json --deltas-only` or `openspec show --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 `, `ls`, or direct file reads so proposals align with current implementation + realities. + + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..cf03a47a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,21 @@ + + +# 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. + + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 0e3bdbd9..4f76ac5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,25 @@ + + +# 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. + + + # CLAUDE.md 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 - `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) - `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 ./internal/logic/card_apple_account -v` - Run tests for specific module - `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 - `make image` - Build Docker image with auto-generated git-based tag - `make image.push` - Build and push Docker image - `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 @@ -79,6 +111,7 @@ prefixes. ### Technology Stack Details - **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 - **Cache**: Redis for caching, sessions, rate limiting - **Tracing**: OpenTelemetry with custom headers (`x-service-token`) diff --git a/CONTROLLER_GENERATION_REPORT.md b/CONTROLLER_GENERATION_REPORT.md index 360de20c..71c5d6a6 100644 --- a/CONTROLLER_GENERATION_REPORT.md +++ b/CONTROLLER_GENERATION_REPORT.md @@ -23,20 +23,20 @@ gf gen ctrl **修复前后对比**: -| 生成的接口方法 | 原控制器方法 | 修复后控制器方法 | 状态 | -|----------------|--------------|------------------|------| -| `CreateAccount` | `CreateAccount` | `CreateAccount` | ✅ 匹配 | -| `BatchCreate` | `BatchCreateAccount` | `BatchCreate` | ✅ 已修复 | -| `ListAccount` | `ListAccount` | `ListAccount` | ✅ 匹配 | -| `UpdateAccount` | `UpdateAccount` | `UpdateAccount` | ✅ 匹配 | -| `DeleteAccount` | `DeleteAccount` | `DeleteAccount` | ✅ 匹配 | -| `BatchCheck` | `BatchCheckAccount` | `BatchCheck` | ✅ 已修复 | -| `CreateOrder` | `CreateOrder` | `CreateOrder` | ✅ 匹配 | -| `GetPaymentUrl` | `GetPaymentUrl` | `GetPaymentUrl` | ✅ 匹配 | -| `GetOrderStatus` | `GetOrderStatus` | `GetOrderStatus` | ✅ 匹配 | -| `ListOrder` | `ListOrder` | `ListOrder` | ✅ 匹配 | -| `CookieHistory` | `GetCookieHistory` | `CookieHistory` | ✅ 已修复 | -| `OrderHistory` | `GetOrderHistory` | `OrderHistory` | ✅ 已修复 | +| 生成的接口方法 | 原控制器方法 | 修复后控制器方法 | 状态 | +|------------------|----------------------|------------------|-------| +| `CreateAccount` | `CreateAccount` | `CreateAccount` | ✅ 匹配 | +| `BatchCreate` | `BatchCreateAccount` | `BatchCreate` | ✅ 已修复 | +| `ListAccount` | `ListAccount` | `ListAccount` | ✅ 匹配 | +| `UpdateAccount` | `UpdateAccount` | `UpdateAccount` | ✅ 匹配 | +| `DeleteAccount` | `DeleteAccount` | `DeleteAccount` | ✅ 匹配 | +| `BatchCheck` | `BatchCheckAccount` | `BatchCheck` | ✅ 已修复 | +| `CreateOrder` | `CreateOrder` | `CreateOrder` | ✅ 匹配 | +| `GetPaymentUrl` | `GetPaymentUrl` | `GetPaymentUrl` | ✅ 匹配 | +| `GetOrderStatus` | `GetOrderStatus` | `GetOrderStatus` | ✅ 匹配 | +| `ListOrder` | `ListOrder` | `ListOrder` | ✅ 匹配 | +| `CookieHistory` | `GetCookieHistory` | `CookieHistory` | ✅ 已修复 | +| `OrderHistory` | `GetOrderHistory` | `OrderHistory` | ✅ 已修复 | ### 4. 验证结果 diff --git a/api/jd_cookie/v1/order.go b/api/jd_cookie/v1/order.go index d56cffd2..4ec46b40 100644 --- a/api/jd_cookie/v1/order.go +++ b/api/jd_cookie/v1/order.go @@ -90,13 +90,17 @@ type GetJdOrderRes struct { } type OrderInfo struct { + Id int64 `json:"id" dc:"主键ID"` OrderId string `json:"orderId" dc:"订单号"` + UserOrderId string `json:"userOrderId" dc:"用户订单号"` Amount float64 `json:"amount" dc:"订单金额"` Category string `json:"category" dc:"商品品类"` JdOrderId string `json:"jdOrderId" dc:"关联的京东订单号"` Status consts.OrderStatus `json:"status" dc:"状态:1待支付 2已支付 3已过期 4已取消"` LastRequest *gtime.Time `json:"lastRequestAt" dc:"最后请求时间"` CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` + DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"` } type JdOrderInfo struct { @@ -111,7 +115,7 @@ type JdOrderInfo struct { WxPayUrl string `json:"wxPayUrl" dc:"微信支付链接"` WxPayExpireAt *gtime.Time `json:"wxPayExpireAt" dc:"微信支付链接过期时间"` OrderExpireAt *gtime.Time `json:"orderExpireAt" dc:"订单过期时间(默认24小时)"` - CurrentOrderId int64 `json:"currentOrderId" dc:"当前关联的订单ID"` + OrderId string `json:"orderId" dc:"关联的用户订单号"` PaidAt *gtime.Time `json:"paidAt" dc:"支付完成时间"` CardNo string `json:"cardNo" dc:"卡号"` CardPassword string `json:"cardPassword" dc:"卡密"` @@ -119,7 +123,6 @@ type JdOrderInfo struct { CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"` - OrderId string `json:"orderId" dc:"关联的用户订单号"` } // ListOrderReq Order List Query Request diff --git a/internal/boot/boot_enums.go b/internal/boot/boot_enums.go index b3eb0f57..24142645 100644 --- a/internal/boot/boot_enums.go +++ b/internal/boot/boot_enums.go @@ -9,5 +9,5 @@ import ( ) func init() { - gtag.SetGlobalEnums(`{"github.com/gogf/gf/v2/database/gdb.InsertOption":[0,3,1,2],"github.com/gogf/gf/v2/database/gdb.LocalType":["bigint","bool","[]byte","date","datetime","float32","float64","int","int64","int64-bytes","[]int64","[]int","json","jsonb","string","[]string","time","uint","uint64","uint64-bytes","[]uint64",""],"github.com/gogf/gf/v2/database/gdb.Propagation":["MANDATORY","NESTED","NEVER","NOT_SUPPORTED","REQUIRED","REQUIRES_NEW","SUPPORTS"],"github.com/gogf/gf/v2/database/gdb.Role":["master","slave"],"github.com/gogf/gf/v2/database/gdb.SelectType":[3,1,0,2],"github.com/gogf/gf/v2/database/gdb.SoftTimeType":[0,1,2,4,3,5],"github.com/gogf/gf/v2/database/gdb.SqlType":["DB.Begin","DB.ExecContext","DB.PrepareContext","DB.QueryContext","DB.Statement.ExecContext","DB.Statement.QueryContext","DB.Statement.QueryRowContext","TX.Commit","TX.Rollback"],"github.com/gogf/gf/v2/database/gredis.FlushOp":["ASYNC","SYNC"],"github.com/gogf/gf/v2/database/gredis.LInsertOp":["AFTER","BEFORE"],"github.com/gogf/gf/v2/encoding/gjson.ContentType":["ini","json","js","properties","toml","xml","yaml","yml"],"github.com/gogf/gf/v2/internal/errors.StackMode":["brief","detail"],"github.com/gogf/gf/v2/net/ghttp.HandlerType":["handler","hook","middleware","object"],"github.com/gogf/gf/v2/net/ghttp.HookName":["HOOK_AFTER_OUTPUT","HOOK_AFTER_SERVE","HOOK_BEFORE_OUTPUT","HOOK_BEFORE_SERVE"],"github.com/gogf/gf/v2/net/ghttp/internal/graceful.ServerStatus":[1,0,1,0],"github.com/gogf/gf/v2/os/gctx.StrKey":["CtxKeyArgumentsIndex","CtxKeyCommand","CtxKeyParser"],"github.com/gogf/gf/v2/os/gfsnotify.Op":[16,1,4,8,2],"github.com/gogf/gf/v2/os/gmetric.MetricType":["Counter","Histogram","ObservableCounter","ObservableGauge","ObservableUpDownCounter","UpDownCounter"],"github.com/gogf/gf/v2/os/gstructs.RecursiveOption":[1,2,0],"github.com/gogf/gf/v2/text/gstr.CaseType":["Camel","CamelLower","Kebab","KebabScreaming","Lower","Snake","SnakeFirstUpper","SnakeScreaming"],"github.com/gogf/gf/v2/util/gconv/internal/converter.RecursiveType":["auto","true"],"kami/internal/consts.AgisoCallbackStatus":[16,2097152,4,2048,524288,256,32768,1048576],"kami/internal/consts.AppleAccountStatus":[1,5,6,8,4,2,9,7,3],"kami/internal/consts.AppleOrderItunesStatus":[30,31,40,32,10,12,11,14,20,13,15],"kami/internal/consts.AppleOrderOperation":["回调网关失败","订单正在处理中或者等待处理~","回调网关成功","iTunes回调次数超限","创建订单","账户余额查询","创建订单(人工处理订单,需人工介入)","手动修正金额成功","手动回调成功","正在处理","iTunes充值失败(错误未知)","iTunes退回订单,等待重新调度","iTunes充值失败(卡密已兑换)","iTunes充值成功","iTunes处理成功(金额异议)","iTunes充值失败(卡密无效)","iTunes充值失败(商店匹配错误)","重复操作","重置订单状态","iTunes开始处理","iTunes充值订单处理超时","代充值账户密码错误,等待重新调度"],"kami/internal/consts.AppleRechargeOrderStatus":[13,15,6,14,5,0,16,4,2,9,12,10,11,7,8,1,3],"kami/internal/consts.CardAppleNotifyStatus":[2,1],"kami/internal/consts.CardCookieJDOrderStatus":["ckFailed","expired","init","failed","success","wrongFacePrice"],"kami/internal/consts.CardCookieUserClient":["android","ios","web"],"kami/internal/consts.CardJDNotifyStatus":[2,1],"kami/internal/consts.CardRedeemAccountCategory":["apple","cTrip","jd","originalJD","walmart"],"kami/internal/consts.CardRedeemCookieCategory":["jd"],"kami/internal/consts.CardRedeemCookieOrderStatus":["init","placeFail","placeSuccess"],"kami/internal/consts.CardRedeemCookieStatus":["dailyDisable","disable","expired","normal","tmpDisable"],"kami/internal/consts.CardRedeemType":["apple","jd","TMall"],"kami/internal/consts.CardTMallGameNotifyStatus":[2,1],"kami/internal/consts.CookieChangeType":["create","delete","fail","refresh fail","replaced","resume","suspend","update","use"],"kami/internal/consts.DeductionStatus":["fail","return","start","success"],"kami/internal/consts.EnumShopStatus":["bind_order_fail","bind_order_succeed","confirm_order","delivery_failed","delivery_succeed","evaluated_failed","evaluated_succeed","evaluation_bad","evaluation_good","paid","receive_callback"],"kami/internal/consts.JDAccountOperationStatus":["add","deduct","initialize","return"],"kami/internal/consts.JDAccountStatus":[0,5,3,6,2,4,1,7],"kami/internal/consts.JDOrderOperationStatus":[1,4,11,9,10,0,6,8,2,7,5,3],"kami/internal/consts.JDOrderStatus":[0,4,6,10,5,9,7,2,10,3,1,8],"kami/internal/consts.JdCookieStatus":[3,1,2],"kami/internal/consts.JdOrderChangeType":["bind","create","expire","invalid","pay","send","unbind"],"kami/internal/consts.JdOrderStatus":[5,4,2,1,3],"kami/internal/consts.OrderChangeType":["create","expire","pay","rebind"],"kami/internal/consts.OrderStatus":[4,3,2,1],"kami/internal/consts.PageSize":[10,100,20,50],"kami/internal/consts.RechargeTMallGameAccount":["disable","enable"],"kami/internal/consts.RechargeTMallGameCallBackType":["confirm","evaluation"],"kami/internal/consts.RechargeTMallGameOrder":["bind_shop_succeed","callback_failed","callback_manual_failed_manuel","callback_succeed","callback_manual_succeed_manuel","evaluation","callback","created","delivery_failed","delivery_succeed","finished","finished_with_refund_succeed","finished_with_wrong_amount","finished_with_wrong_status","paid","refund_failed","wait_for_evaluation","without_fill_account","trade_rated_add_failed","trade_rated_add_succeed"],"kami/internal/consts.RechargeTMallGameShopSourceType":["agiso","tMall"],"kami/internal/consts.RedeemAccountOperationStatus":["add","deduct","initialize","pre_add","pre_deduct","return"],"kami/internal/consts.RedeemAccountStatus":[0,5,3,6,2,8,4,1,7],"kami/internal/consts.RedeemAccountUsedStatus":[false,true],"kami/internal/consts.RedeemCardScheduleStrategyType":["normal","random"],"kami/internal/consts.RedeemOrderCallbackStatus":[2,1],"kami/internal/consts.RedeemOrderCardCategory":["apple","cTrip","walmart"],"kami/internal/consts.RedeemOrderOperationStatus":[1,4,11,12,13,9,10,19,100,6,15,8,2,7,17,5,16,18,3,14],"kami/internal/consts.RedeemOrderStatus":[100,15,19,20,4,16,6,13,10,5,21,11,12,9,7,2,14,3,18,1,17,8],"kami/internal/consts.RestrictStatus":[0,1],"kami/internal/consts.StatusType":["no","yes"],"kami/internal/consts.StealStatus":[0,1],"kami/internal/consts.SysConfigDictType":["account_max_recharge_count","is_steal_apple_card","is_steal_merchant_card","redeem_allow_repeated","redeem_card_compensated_auto_callback","redeem_card_different_amount","redeem_card_different_fail_callback_allow","redeem_card_different_succeed_callback_allow","redeem_card_min_amount","redeem_card_rate","redeem_redeem_max_count_limit","redeem_schedule_strategy","steal_rule_status"],"kami/internal/consts.SysUserStatus":[0,1],"kami/internal/consts.TMallGameCallbackStatus":[0,1],"kami/internal/consts.TMallGameThirdPartyChannel":["12352","12351"],"kami/internal/consts.UserPaymentStatus":[0,1],"kami/internal/consts.UserPaymentTransactionType":["consumption","deduction_return","Manual Adjustment","deposit"],"kami/internal/model.LoginType":["admin","auth","merchant"],"kami/utility/cache.CachedEnum":["apple_account_target_account_id_by_account","apple_account_target_account_id_by_user","apple_account_tmp_stopped","itunes_account_tmp_stopped","redeem_account_target_id_by_account","redeem_account_target_account_id_by_ck_and_user","redeem_account_target_id_by_user","redeem_account_tmp_stopped","tMallGameOrderTid","tMallGameCacheKeyAgiso","tMallGameCacheKeyTMall"],"kami/utility/cache.PrefixEnum":["account_limiter_type","apple_duplicated_order","MachineCurrentAccountId","jd_account_query_cache_with_cookie","jd_account_query_cache","jd_card_extract","jd_payment_check","redeem_apple_account_limited_type","redeem_type","redeem_with_payment_type","trace","walmart_account_query_cache_with_cookie","walmart_account_query_cache"],"kami/utility/integration/originalJd.HttpStatus":[201,202,500,200,204,203,100],"kami/utility/integration/redeem.FailType":[1,2],"kami/utility/integration/redeem/ctrip.BindCardType":[113,105,110,114,104,100,115],"kami/utility/integration/redeem/jd.CallbackResponseStatus":[113,115,104,107,106,111,105,110,120,110,100,201,101],"kami/utility/integration/redeem/walmart.BindCardType":[1013,1006,1016,1100,1110,1005,1004,1000,1015],"kami/utility/integration/restriction.ConstsImpl":["csdn","dbip","ip66","iqiyi","idcd","meitu","olt","PCOnline","qjqq","vo"],"kami/utility/integration/tmall.EnumRateStatus":["RATE_BUYER_SELLER","RATE_BUYER_UNSELLER","RATE_UNBUYER","RATE_UNBUYER_SELLER","RATE_UNSELLER"],"kami/utility/integration/tmall.EnumTradeStatus":["PAID_FORBID_CONSIGN","PAY_PENDING","SELLER_CONSIGNED_PART","TRADE_BUYER_SIGNED","TRADE_CLOSED","TRADE_CLOSED_BY_TAOBAO","TRADE_FINISHED","TRADE_NO_CREATE_PAY","WAIT_BUYER_CONFIRM_GOODS","WAIT_BUYER_PAY","WAIT_PRE_AUTH_CONFIRM","WAIT_SELLER_SEND_GOODS"],"kami/utility/limiter.Type":["cardInfo:jd:account:cookie","cardInfo:jd:account:cookie:set","cardInfo:account:cookie:checker","cardInfo:account:cookie:set"],"kami/utility/pool.Key":["account_detect","apple_account_check_wallet","apple_card_callback","apple_card_t_mall_callback","jd_card_callback","jd_card_consume","redeem_card_callback","redeem_card_consume","t_mall_game_account_callback"]}`) + gtag.SetGlobalEnums(`{"github.com/gogf/gf/v2/database/gdb.InsertOption":[0,3,1,2],"github.com/gogf/gf/v2/database/gdb.LocalType":["bigint","bool","[]byte","date","datetime","float32","float64","int","int64","int64-bytes","[]int64","[]int","json","jsonb","string","[]string","time","uint","uint64","uint64-bytes","[]uint64",""],"github.com/gogf/gf/v2/database/gdb.Propagation":["MANDATORY","NESTED","NEVER","NOT_SUPPORTED","REQUIRED","REQUIRES_NEW","SUPPORTS"],"github.com/gogf/gf/v2/database/gdb.Role":["master","slave"],"github.com/gogf/gf/v2/database/gdb.SelectType":[3,1,0,2],"github.com/gogf/gf/v2/database/gdb.SoftTimeType":[0,1,2,4,3,5],"github.com/gogf/gf/v2/database/gdb.SqlType":["DB.Begin","DB.ExecContext","DB.PrepareContext","DB.QueryContext","DB.Statement.ExecContext","DB.Statement.QueryContext","DB.Statement.QueryRowContext","TX.Commit","TX.Rollback"],"github.com/gogf/gf/v2/database/gredis.FlushOp":["ASYNC","SYNC"],"github.com/gogf/gf/v2/database/gredis.LInsertOp":["AFTER","BEFORE"],"github.com/gogf/gf/v2/encoding/gjson.ContentType":["ini","json","js","properties","toml","xml","yaml","yml"],"github.com/gogf/gf/v2/internal/errors.StackMode":["brief","detail"],"github.com/gogf/gf/v2/net/ghttp.HandlerType":["handler","hook","middleware","object"],"github.com/gogf/gf/v2/net/ghttp.HookName":["HOOK_AFTER_OUTPUT","HOOK_AFTER_SERVE","HOOK_BEFORE_OUTPUT","HOOK_BEFORE_SERVE"],"github.com/gogf/gf/v2/net/ghttp/internal/graceful.ServerStatus":[1,0,1,0],"github.com/gogf/gf/v2/os/gctx.StrKey":["CtxKeyArgumentsIndex","CtxKeyCommand","CtxKeyParser"],"github.com/gogf/gf/v2/os/gfsnotify.Op":[16,1,4,8,2],"github.com/gogf/gf/v2/os/gmetric.MetricType":["Counter","Histogram","ObservableCounter","ObservableGauge","ObservableUpDownCounter","UpDownCounter"],"github.com/gogf/gf/v2/os/gstructs.RecursiveOption":[1,2,0],"github.com/gogf/gf/v2/text/gstr.CaseType":["Camel","CamelLower","Kebab","KebabScreaming","Lower","Snake","SnakeFirstUpper","SnakeScreaming"],"github.com/gogf/gf/v2/util/gconv/internal/converter.RecursiveType":["auto","true"],"kami/internal/consts.AgisoCallbackStatus":[16,2097152,4,2048,524288,256,32768,1048576],"kami/internal/consts.AppleAccountStatus":[1,5,6,8,4,2,9,7,3],"kami/internal/consts.AppleOrderItunesStatus":[30,31,40,32,10,12,11,14,20,13,15],"kami/internal/consts.AppleOrderOperation":["回调网关失败","订单正在处理中或者等待处理~","回调网关成功","iTunes回调次数超限","创建订单","账户余额查询","创建订单(人工处理订单,需人工介入)","手动修正金额成功","手动回调成功","正在处理","iTunes充值失败(错误未知)","iTunes退回订单,等待重新调度","iTunes充值失败(卡密已兑换)","iTunes充值成功","iTunes处理成功(金额异议)","iTunes充值失败(卡密无效)","iTunes充值失败(商店匹配错误)","重复操作","重置订单状态","iTunes开始处理","iTunes充值订单处理超时","代充值账户密码错误,等待重新调度"],"kami/internal/consts.AppleRechargeOrderStatus":[13,15,6,14,5,0,16,4,2,9,12,10,11,7,8,1,3],"kami/internal/consts.CardAppleNotifyStatus":[2,1],"kami/internal/consts.CardCookieJDOrderStatus":["ckFailed","expired","init","failed","riskFailed","success","wrongFacePrice"],"kami/internal/consts.CardCookieUserClient":["android","ios","web"],"kami/internal/consts.CardJDNotifyStatus":[2,1],"kami/internal/consts.CardRedeemAccountCategory":["apple","cTrip","jd","originalJD","walmart"],"kami/internal/consts.CardRedeemCookieCategory":["jd"],"kami/internal/consts.CardRedeemCookieOrderStatus":["init","placeFail","placeSuccess"],"kami/internal/consts.CardRedeemCookieStatus":["dailyDisable","disable","expired","normal","tmpDisable"],"kami/internal/consts.CardRedeemType":["apple","jd","TMall"],"kami/internal/consts.CardTMallGameNotifyStatus":[2,1],"kami/internal/consts.CookieChangeType":["create","delete","fail","refresh fail","replaced","resume","suspend","update","use"],"kami/internal/consts.DeductionStatus":["fail","return","start","success"],"kami/internal/consts.EnumShopStatus":["bind_order_fail","bind_order_succeed","confirm_order","delivery_failed","delivery_succeed","evaluated_failed","evaluated_succeed","evaluation_bad","evaluation_good","paid","receive_callback"],"kami/internal/consts.JDAccountOperationStatus":["add","deduct","initialize","return"],"kami/internal/consts.JDAccountStatus":[0,5,3,6,2,4,1,7],"kami/internal/consts.JDOrderOperationStatus":[1,4,11,9,10,0,6,8,2,7,5,3],"kami/internal/consts.JDOrderStatus":[0,4,6,10,5,9,7,2,10,3,1,8],"kami/internal/consts.JdCookieStatus":[3,1,2,0],"kami/internal/consts.JdOrderChangeType":["bind","create","expire","invalid","pay","send","unbind"],"kami/internal/consts.JdOrderStatus":[5,6,4,2,1,3],"kami/internal/consts.OrderChangeType":["ck_failed","create","expire","pay","rebind"],"kami/internal/consts.OrderStatus":[4,5,3,2,1],"kami/internal/consts.PageSize":[10,100,20,50],"kami/internal/consts.RechargeTMallGameAccount":["disable","enable"],"kami/internal/consts.RechargeTMallGameCallBackType":["confirm","evaluation"],"kami/internal/consts.RechargeTMallGameOrder":["bind_shop_succeed","callback_failed","callback_manual_failed_manuel","callback_succeed","callback_manual_succeed_manuel","evaluation","callback","created","delivery_failed","delivery_succeed","finished","finished_with_refund_succeed","finished_with_wrong_amount","finished_with_wrong_status","paid","refund_failed","wait_for_evaluation","without_fill_account","trade_rated_add_failed","trade_rated_add_succeed"],"kami/internal/consts.RechargeTMallGameShopSourceType":["agiso","tMall"],"kami/internal/consts.RedeemAccountOperationStatus":["add","deduct","initialize","pre_add","pre_deduct","return"],"kami/internal/consts.RedeemAccountStatus":[0,5,3,6,2,8,4,1,7],"kami/internal/consts.RedeemAccountUsedStatus":[false,true],"kami/internal/consts.RedeemCardScheduleStrategyType":["normal","random"],"kami/internal/consts.RedeemOrderCallbackStatus":[2,1],"kami/internal/consts.RedeemOrderCardCategory":["apple","cTrip","walmart"],"kami/internal/consts.RedeemOrderOperationStatus":[1,4,11,12,13,9,10,19,100,6,15,8,2,7,17,5,16,18,3,14],"kami/internal/consts.RedeemOrderStatus":[100,15,19,20,4,16,6,13,10,5,21,11,12,9,7,2,14,3,18,1,17,8],"kami/internal/consts.RestrictStatus":[0,1],"kami/internal/consts.StatusType":["no","yes"],"kami/internal/consts.StealStatus":[0,1],"kami/internal/consts.SysConfigDictType":["account_max_recharge_count","is_steal_apple_card","is_steal_merchant_card","redeem_allow_repeated","redeem_card_compensated_auto_callback","redeem_card_different_amount","redeem_card_different_fail_callback_allow","redeem_card_different_succeed_callback_allow","redeem_card_min_amount","redeem_card_rate","redeem_redeem_max_count_limit","redeem_schedule_strategy","steal_rule_status"],"kami/internal/consts.SysUserStatus":[0,1],"kami/internal/consts.TMallGameCallbackStatus":[0,1],"kami/internal/consts.TMallGameThirdPartyChannel":["12352","12351"],"kami/internal/consts.UserPaymentStatus":[0,1],"kami/internal/consts.UserPaymentTransactionType":["consumption","deduction_return","Manual Adjustment","deposit"],"kami/internal/model.LoginType":["admin","auth","merchant"],"kami/utility/cache.CachedEnum":["apple_account_target_account_id_by_account","apple_account_target_account_id_by_user","apple_account_tmp_stopped","itunes_account_tmp_stopped","redeem_account_target_id_by_account","redeem_account_target_account_id_by_ck_and_user","redeem_account_target_id_by_user","redeem_account_tmp_stopped","tMallGameOrderTid","tMallGameCacheKeyAgiso","tMallGameCacheKeyTMall"],"kami/utility/cache.PrefixEnum":["account_limiter_type","apple_duplicated_order","MachineCurrentAccountId","jd_account_query_cache_with_cookie","jd_account_query_cache","jd_card_extract","jd_payment_check","redeem_apple_account_limited_type","redeem_type","redeem_with_payment_type","trace","walmart_account_query_cache_with_cookie","walmart_account_query_cache"],"kami/utility/integration/originalJd.HttpStatus":[201,202,500,200,204,203,300,100],"kami/utility/integration/redeem.FailType":[1,2],"kami/utility/integration/redeem/ctrip.BindCardType":[113,105,110,114,104,100,115],"kami/utility/integration/redeem/jd.CallbackResponseStatus":[113,115,104,107,106,111,105,110,120,110,100,201,101],"kami/utility/integration/redeem/walmart.BindCardType":[1013,1006,1016,1100,1110,1005,1004,1000,1015],"kami/utility/integration/restriction.ConstsImpl":["csdn","dbip","ip66","iqiyi","idcd","meitu","olt","PCOnline","qjqq","vo"],"kami/utility/integration/tmall.EnumRateStatus":["RATE_BUYER_SELLER","RATE_BUYER_UNSELLER","RATE_UNBUYER","RATE_UNBUYER_SELLER","RATE_UNSELLER"],"kami/utility/integration/tmall.EnumTradeStatus":["PAID_FORBID_CONSIGN","PAY_PENDING","SELLER_CONSIGNED_PART","TRADE_BUYER_SIGNED","TRADE_CLOSED","TRADE_CLOSED_BY_TAOBAO","TRADE_FINISHED","TRADE_NO_CREATE_PAY","WAIT_BUYER_CONFIRM_GOODS","WAIT_BUYER_PAY","WAIT_PRE_AUTH_CONFIRM","WAIT_SELLER_SEND_GOODS"],"kami/utility/limiter.Type":["cardInfo:jd:account:cookie","cardInfo:jd:account:cookie:set","cardInfo:account:cookie:checker","cardInfo:account:cookie:set"],"kami/utility/pool.Key":["account_detect","apple_account_check_wallet","apple_card_callback","apple_card_t_mall_callback","jd_card_callback","jd_card_consume","redeem_card_callback","redeem_card_consume","t_mall_game_account_callback"]}`) } diff --git a/internal/consts/jd_cookie.go b/internal/consts/jd_cookie.go index 519a849b..bf1f9592 100644 --- a/internal/consts/jd_cookie.go +++ b/internal/consts/jd_cookie.go @@ -19,6 +19,7 @@ var JdCookieStatusText = map[JdCookieStatus]string{ JdCookieStatusNormal: "正常", JdCookieStatusSuspend: "暂停", JdCookieStatusExpired: "失效", + JdCookieStatusUnknown: "未知", } // JdOrderStatus 京东订单状态枚举 @@ -93,6 +94,7 @@ const ( JdOrderChangeTypeExpire JdOrderChangeType = "expire" // 过期 JdOrderChangeTypeInvalid JdOrderChangeType = "invalid" // 失效(新增) JdOrderChangeTypeSend JdOrderChangeType = "send" // 发货 + JdOrderChangeTypeReplace JdOrderChangeType = "replace" // 换绑 ) // JdOrderChangeTypeText 京东订单变更类型文本映射 @@ -104,6 +106,7 @@ var JdOrderChangeTypeText = map[JdOrderChangeType]string{ JdOrderChangeTypeExpire: "过期", JdOrderChangeTypeInvalid: "失效", JdOrderChangeTypeSend: "发货", + JdOrderChangeTypeReplace: "换绑", } // OrderChangeType 订单变更类型 diff --git a/internal/controller/jd_cookie/jd_cookie_v1_create_order.go b/internal/controller/jd_cookie/jd_cookie_v1_create_order.go index cb4801f7..1d543355 100644 --- a/internal/controller/jd_cookie/jd_cookie_v1_create_order.go +++ b/internal/controller/jd_cookie/jd_cookie_v1_create_order.go @@ -3,6 +3,7 @@ package jd_cookie import ( "context" "kami/api/jd_cookie/v1" + "kami/internal/model" "kami/internal/service" "github.com/gogf/gf/v2/errors/gcode" @@ -11,7 +12,12 @@ import ( // CreateOrder 创建订单 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 { return nil, gerror.WrapCode(gcode.CodeInternalError, err, "创建订单失败") } diff --git a/internal/dao/internal/v_1_jd_cookie_jd_order.go b/internal/dao/internal/v_1_jd_cookie_jd_order.go index d87585db..3c580555 100644 --- a/internal/dao/internal/v_1_jd_cookie_jd_order.go +++ b/internal/dao/internal/v_1_jd_cookie_jd_order.go @@ -32,7 +32,7 @@ type V1JdCookieJdOrderColumns struct { WxPayUrl string // 微信支付链接 WxPayExpireAt string // 微信支付链接过期时间 OrderExpireAt string // 订单过期时间(默认24小时) - CurrentOrderId string // 当前关联的订单ID + OrderId string // 关联的用户订单号 PaidAt string // 支付完成时间 CardNo string // 卡号 CardPassword string // 卡密 @@ -55,7 +55,7 @@ var v1JdCookieJdOrderColumns = V1JdCookieJdOrderColumns{ WxPayUrl: "wx_pay_url", WxPayExpireAt: "wx_pay_expire_at", OrderExpireAt: "order_expire_at", - CurrentOrderId: "current_order_id", + OrderId: "order_id", PaidAt: "paid_at", CardNo: "card_no", CardPassword: "card_password", diff --git a/internal/logic/jd_cookie/README.md b/internal/logic/jd_cookie/README.md index d1fdfb29..08e96bc3 100644 --- a/internal/logic/jd_cookie/README.md +++ b/internal/logic/jd_cookie/README.md @@ -51,12 +51,19 @@ amount := 100.0 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 { // 所有 Cookie 都失败,返回错误 // 错误信息会包含:已尝试的 Cookie 数量 return err } +wxPayUrl := result.WxPayUrl +jdOrderId := result.JdOrderId // 成功返回支付链接 ``` diff --git a/internal/logic/jd_cookie/order_create.go b/internal/logic/jd_cookie/order_create.go index 624b437c..a105df71 100644 --- a/internal/logic/jd_cookie/order_create.go +++ b/internal/logic/jd_cookie/order_create.go @@ -15,18 +15,18 @@ import ( ) // 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) - if userOrderId == "" { + if req.UserOrderId == "" { return nil, gerror.New("用户订单号不能为空") } - if amount <= 0 { + if req.Amount <= 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) if err != nil { 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 { 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 { glog.Warning(ctx, "查找可复用京东订单失败", err) } @@ -136,8 +136,8 @@ func (s *sJdCookie) CreateOrder(ctx context.Context, userOrderId string, amount if jdOrderId == "" { retryRes, err := s.createNewJdOrderWithRetry(ctx, &model.CreateNewJdOrderWithRetryReq{ OrderId: internalOrderId, - Amount: amount, - Category: category, + Amount: req.Amount, + Category: req.Category, }) if err != nil { 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 { return nil, gerror.Wrap(err, "创建订单记录失败") } // 更新京东订单的当前关联订单ID - _ = s.updateJdOrderCurrentOrderId(ctx, jdOrderId, internalOrderId) + _ = s.updateJdOrderId(ctx, jdOrderId, internalOrderId) // 记录Cookie使用历史 _ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{ CookieId: cookieId, ChangeType: consts.CookieChangeTypeUse, - StatusBefore: consts.JdCookieStatusNormal, + StatusBefore: consts.JdCookieStatusUnknown, StatusAfter: consts.JdCookieStatusNormal, UserOrderId: internalOrderId, FailureCount: 0, diff --git a/internal/logic/jd_cookie/order_jd.go b/internal/logic/jd_cookie/order_jd.go index de0800de..d56d8215 100644 --- a/internal/logic/jd_cookie/order_jd.go +++ b/internal/logic/jd_cookie/order_jd.go @@ -49,7 +49,7 @@ func (s *sJdCookie) GetJdOrder(ctx context.Context, jdOrderId string) (order *v1 WxPayUrl: jdOrderEntity.WxPayUrl, WxPayExpireAt: jdOrderEntity.WxPayExpireAt, OrderExpireAt: jdOrderEntity.OrderExpireAt, - CurrentOrderId: jdOrderEntity.CurrentOrderId, + OrderId: jdOrderEntity.OrderId, CardNo: jdOrderEntity.CardNo, CardPassword: jdOrderEntity.CardPassword, 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 } @@ -112,17 +101,8 @@ func (s *sJdCookie) ListJdOrder(ctx context.Context, page, size int, status cons m = m.WhereLTE(dao.V1JdCookieJdOrder.Columns().CreatedAt, endTime) } if orderId != "" { - // 先查询对应的内部订单ID,然后筛选 - var userOrder *entity.V1JdCookieOrder - 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 - } + // 直接根据order_id字段筛选 + m = m.Where(dao.V1JdCookieJdOrder.Columns().OrderId, orderId) } // 查询总数 @@ -176,7 +156,7 @@ func (s *sJdCookie) ListJdOrder(ctx context.Context, page, size int, status cons WxPayUrl: jdOrderEntity.WxPayUrl, WxPayExpireAt: jdOrderEntity.WxPayExpireAt, OrderExpireAt: jdOrderEntity.OrderExpireAt, - CurrentOrderId: jdOrderEntity.CurrentOrderId, + OrderId: jdOrderEntity.OrderId, PaidAt: jdOrderEntity.PaidAt, CardNo: jdOrderEntity.CardNo, 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) } @@ -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{ "jdOrderId": jdOrderId, "status": jdOrder, @@ -459,14 +428,6 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error 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信息 cookieInfo, err := s.getCookieById(ctx, jdOrder.CookieId) if err != nil || cookieInfo == nil { @@ -484,13 +445,12 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error if resp.IsCkFailed { s.handleCookieFailure(ctx, cookieInfo.CookieId, jdOrderId, true, resp.Remark) _ = s.UpdateJdOrderStatus(ctx, jdOrderId, consts.JdOrderStatusCkFailed, "", resp.Remark) - if order != nil { - _, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().Id, order.Id). - Update(do.V1JdCookieOrder{ - Status: consts.OrderStatusCkFailed, - }) - _ = s.RecordOrderHistory(ctx, order.OrderId, consts.OrderChangeTypeCkFailed, jdOrderId) - } + _, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().OrderId, jdOrder.OrderId). + Update(do.V1JdCookieOrder{ + Status: consts.OrderStatusCkFailed, + }) + _ = s.RecordOrderHistory(ctx, jdOrder.OrderId, consts.OrderChangeTypeCkFailed, jdOrderId) + } } return gerror.Wrap(err, "查询京东订单支付状态失败") @@ -507,13 +467,13 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error PaidAt: gtime.Now(), Status: consts.JdOrderStatusPaid, }) - if order != nil { - _ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypePay, order.OrderId, jdOrder.WxPayUrl, "") - _, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().Id, order.Id). + if jdOrder.OrderId != "" { + _ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypePay, jdOrder.OrderId, jdOrder.WxPayUrl, "") + _, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().OrderId, jdOrder.OrderId). Update(do.V1JdCookieOrder{ Status: consts.OrderStatusPaid, }) - _ = s.RecordOrderHistory(ctx, order.OrderId, consts.OrderChangeTypePay, jdOrderId) + _ = s.RecordOrderHistory(ctx, jdOrder.OrderId, consts.OrderChangeTypePay, jdOrderId) } return nil } @@ -521,13 +481,16 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error return gerror.New("获取卡密信息为空") } - if order != nil && order.Status != int(consts.OrderStatusPaid) { - _, _ = dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1JdCookieOrder.Columns().Id, order.Id). - Update(do.V1JdCookieOrder{ - Status: consts.OrderStatusPaid, - }) - _ = s.RecordOrderHistory(ctx, order.OrderId, consts.OrderChangeTypePay, jdOrderId) + affected, _ := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()). + Where(dao.V1JdCookieOrder.Columns().OrderId, jdOrder.OrderId). + WhereNot(dao.V1JdCookieOrder.Columns().Status, consts.OrderStatusPaid). + UpdateAndGetAffected(do.V1JdCookieOrder{ + Status: consts.OrderStatusPaid, + }) + if affected > 0 { + _ = s.RecordOrderHistory(ctx, jdOrder.OrderId, consts.OrderChangeTypePay, jdOrderId) } + // 保存卡密信息到数据库 _, err = dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1()). Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId). @@ -541,39 +504,46 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error 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{ "jdOrderId": jdOrderId, "cardNo": resp.CardNo, - "orderId": order.OrderId, + "orderId": jdOrder.OrderId, }) return nil } // 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 - if err := dao.V1OrderInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1OrderInfo.Columns().MerchantOrderId, userOrderId).Scan(&data); err != nil || data == nil || data.Id == 0 { - glog.Error(ctx, "查询订单失败", g.Map{"userOrderId": userOrderId, "err": err}) + 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": order.UserOrderId, "err": err}) return } response, _ := gclient.New().Get(ctx, "http://kami_gateway:12309/appleCard/notify", g.Map{ "attach": data.BankOrderId, "merchantId": orderId, - "amount": amount, + "amount": order.Amount, "status": "1", "sign": "123456", }) glog.Info(ctx, "回调成功", g.Map{"response": response.ReadAllString()}) } -// ShouldExtractCard 判断是否需要提取卡密 -func (s *sJdCookie) ShouldExtractCard(ctx context.Context, jdOrder *entity.V1JdCookieJdOrder) bool { +// shouldExtractCard 判断是否需要提取卡密 +func (s *sJdCookie) shouldExtractCard(ctx context.Context, jdOrder *entity.V1JdCookieJdOrder) bool { if jdOrder == nil { return false } @@ -621,17 +591,10 @@ func (s *sJdCookie) CleanupExpiredOrders(ctx context.Context) error { // 为每个过期的京东订单记录历史 for _, jdOrder := range expiredJdOrders { - 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 { - orderId = order.OrderId - // 同时记录用户订单的历史 - _ = s.RecordOrderHistory(ctx, orderId, consts.OrderChangeTypeExpire, jdOrder.JdOrderId) - } + orderId := jdOrder.OrderId + if jdOrder.OrderId != "" { + // 同时记录用户订单的历史 + _ = s.RecordOrderHistory(ctx, orderId, consts.OrderChangeTypeExpire, jdOrder.JdOrderId) } _ = 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()) _, err := jdOrderModel. Where(dao.V1JdCookieJdOrder.Columns().Status, int(consts.JdOrderStatusPending)). - WhereNotNull(dao.V1JdCookieJdOrder.Columns().CurrentOrderId). + WhereNotNull(dao.V1JdCookieJdOrder.Columns().OrderId). WhereLT(dao.V1JdCookieJdOrder.Columns().CreatedAt, expireTime). - Update(g.Map{ - dao.V1JdCookieJdOrder.Columns().CurrentOrderId: nil, + Update(do.V1JdCookieJdOrder{ + OrderId: nil, }) if err != nil { 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) } if orderId != "" { - // 先查询对应的内部订单ID,然后筛选 - var userOrder *entity.V1JdCookieOrder - 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) - } + // 直接根据order_id字段筛选 + m = m.Where(dao.V1JdCookieJdOrder.Columns().OrderId, orderId) } // 查询所有符合条件的订单 @@ -778,17 +732,7 @@ func (s *sJdCookie) ExportJdOrder(ctx context.Context, status consts.JdOrderStat f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), jdOrder.JdOrderId) // 用户订单号 - userOrderId := "" - 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("B%d", row), jdOrder.OrderId) // 卡号 f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), jdOrder.CardNo) diff --git a/internal/logic/jd_cookie/order_query.go b/internal/logic/jd_cookie/order_query.go index fd87fa91..2b9ee2a8 100644 --- a/internal/logic/jd_cookie/order_query.go +++ b/internal/logic/jd_cookie/order_query.go @@ -50,9 +50,8 @@ func (s *sJdCookie) GetPaymentUrl(ctx context.Context, orderId string) (result * CookieId: jdOrder.CookieId, }) if isCkFailed { - s.handleCookieFailure(ctx, jdOrder.CookieId, jdOrder.JdOrderId, isCkFailed, "获取支付链接失败") + s.handleCookieFailure(ctx, jdOrder.CookieId, jdOrder.JdOrderId, isCkFailed, refreshErr.Error()) } - if refreshErr != nil { // 刷新失败,标记旧订单为失效 _ = 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{ @@ -83,20 +82,22 @@ func (s *sJdCookie) GetPaymentUrl(ctx context.Context, orderId string) (result * _ = s.updateOrderJdOrderId(ctx, orderId, retryRes.JdOrderId, retryRes.WxPayUrl) // 更新京东订单的当前关联订单ID - _ = s.updateJdOrderCurrentOrderId(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.updateJdOrderId(ctx, retryRes.JdOrderId, orderId) // 记录订单重新绑定历史 - _ = 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 @@ -133,13 +134,17 @@ func (s *sJdCookie) GetOrder(ctx context.Context, orderId string) (order *v1.Ord } order = &v1.OrderInfo{ + Id: orderEntity.Id, OrderId: orderEntity.OrderId, + UserOrderId: orderEntity.UserOrderId, Amount: gconv.Float64(orderEntity.Amount), Category: orderEntity.Category, JdOrderId: orderEntity.JdOrderId, Status: consts.OrderStatus(orderEntity.Status), LastRequest: orderEntity.LastRequestAt, CreatedAt: orderEntity.CreatedAt, + UpdatedAt: orderEntity.UpdatedAt, + DeletedAt: orderEntity.DeletedAt, } return @@ -160,13 +165,17 @@ func (s *sJdCookie) GetOrderStatus(ctx context.Context, orderId string) (order * } order = &v1.OrderInfo{ + Id: orderEntity.Id, OrderId: orderEntity.OrderId, + UserOrderId: orderEntity.UserOrderId, Amount: gconv.Float64(orderEntity.Amount), Category: orderEntity.Category, JdOrderId: orderEntity.JdOrderId, Status: consts.OrderStatus(orderEntity.Status), LastRequest: orderEntity.LastRequestAt, CreatedAt: orderEntity.CreatedAt, + UpdatedAt: orderEntity.UpdatedAt, + DeletedAt: orderEntity.DeletedAt, } return @@ -211,13 +220,17 @@ func (s *sJdCookie) ListOrder(ctx context.Context, page, size int, status consts list = make([]*v1.OrderInfo, 0, len(orders)) for _, orderEntity := range orders { info := &v1.OrderInfo{ + Id: orderEntity.Id, OrderId: orderEntity.OrderId, + UserOrderId: orderEntity.UserOrderId, Amount: gconv.Float64(orderEntity.Amount), Category: orderEntity.Category, JdOrderId: orderEntity.JdOrderId, Status: consts.OrderStatus(orderEntity.Status), LastRequest: orderEntity.LastRequestAt, CreatedAt: orderEntity.CreatedAt, + UpdatedAt: orderEntity.UpdatedAt, + DeletedAt: orderEntity.DeletedAt, } list = append(list, info) } diff --git a/internal/logic/jd_cookie/order_test.go b/internal/logic/jd_cookie/order_test.go index 0e237ac3..7e82b67a 100644 --- a/internal/logic/jd_cookie/order_test.go +++ b/internal/logic/jd_cookie/order_test.go @@ -18,47 +18,47 @@ func TestShouldExtractCard(t *testing.T) { ctx := context.Background() // 测试空订单 - result := s.ShouldExtractCard(ctx, nil) + result := s.shouldExtractCard(ctx, nil) t.Assert(result, false) // 测试未支付订单 jdOrder := &entity.V1JdCookieJdOrder{ Status: int(consts.JdOrderStatusPending), } - result = s.ShouldExtractCard(ctx, jdOrder) + result = s.shouldExtractCard(ctx, jdOrder) t.Assert(result, true) // 待支付状态可以提取卡密 // 测试已支付订单 jdOrder.Status = int(consts.JdOrderStatusPaid) - result = s.ShouldExtractCard(ctx, jdOrder) + result = s.shouldExtractCard(ctx, jdOrder) t.Assert(result, true) // 已支付状态可以提取卡密 // 测试已支付且有支付时间的订单 now := gtime.Now() jdOrder.PaidAt = now - result = s.ShouldExtractCard(ctx, jdOrder) + result = s.shouldExtractCard(ctx, jdOrder) t.Assert(result, true) // 测试已经提取过卡密的订单 jdOrder.CardNo = "1234567890" - result = s.ShouldExtractCard(ctx, jdOrder) + result = s.shouldExtractCard(ctx, jdOrder) t.Assert(result, false) // 测试卡密不完整的订单(只有卡密没有卡号) jdOrder.CardNo = "" jdOrder.CardPassword = "password123" - result = s.ShouldExtractCard(ctx, jdOrder) + result = s.shouldExtractCard(ctx, jdOrder) t.Assert(result, false) // 测试其他状态(已发货) jdOrder.CardPassword = "" jdOrder.Status = int(consts.JdOrderStatusSent) - result = s.ShouldExtractCard(ctx, jdOrder) + result = s.shouldExtractCard(ctx, jdOrder) t.Assert(result, false) // 测试其他状态(已过期) jdOrder.Status = int(consts.JdOrderStatusExpired) - result = s.ShouldExtractCard(ctx, jdOrder) + result = s.shouldExtractCard(ctx, jdOrder) t.Assert(result, false) }) } diff --git a/internal/logic/jd_cookie/order_utils.go b/internal/logic/jd_cookie/order_utils.go index 427bb477..dba545b7 100644 --- a/internal/logic/jd_cookie/order_utils.go +++ b/internal/logic/jd_cookie/order_utils.go @@ -153,71 +153,27 @@ func (s *sJdCookie) updateJdOrderPaymentUrl(ctx context.Context, jdOrderId, wxPa // 记录支付链接更新历史 if oldOrder != nil && oldOrder.WxPayUrl != wxPayUrl { - 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 - } - } - _ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, orderId, wxPayUrl, "") + _ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, oldOrder.OrderId, wxPayUrl, "") } return err } -// updateJdOrderCurrentOrderId 更新京东订单的当前关联订单ID -func (s *sJdCookie) updateJdOrderCurrentOrderId(ctx context.Context, jdOrderId, orderId string) error { +// updateJdOrderId 更新京东订单关联的用户订单ID +func (s *sJdCookie) updateJdOrderId(ctx context.Context, jdOrderId, orderId string) error { 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 - 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 { glog.Warning(ctx, "查询京东订单失败,无法记录历史", err) // 即使查询失败也继续更新 } + // 更新订单ID _, err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Update(&do.V1JdCookieJdOrder{ - CurrentOrderId: order.Id, + OrderId: orderId, }) if err != nil { return err @@ -226,10 +182,10 @@ func (s *sJdCookie) updateJdOrderCurrentOrderId(ctx context.Context, jdOrderId, // 记录订单绑定历史 var changeType consts.JdOrderChangeType if oldJdOrder != nil { - if oldJdOrder.CurrentOrderId == 0 { + if oldJdOrder.OrderId == "" { changeType = consts.JdOrderChangeTypeBind // 首次绑定 - } else if oldJdOrder.CurrentOrderId != order.Id { - changeType = consts.JdOrderChangeTypeSend // 更换绑定 + } else if oldJdOrder.OrderId != orderId { + changeType = consts.JdOrderChangeTypeReplace // 更换绑定 } } else { changeType = consts.JdOrderChangeTypeBind // 首次绑定 @@ -256,12 +212,12 @@ func (s *sJdCookie) findReusableJdOrder(ctx context.Context, amount float64, cat // 查找符合条件的京东订单: // 1. 金额和品类相同 // 2. 状态为待支付 - // 3. 没有当前关联订单或者关联订单已过期 + // 3. 没有关联订单 // 4. 订单未过期 err = m.Where(dao.V1JdCookieJdOrder.Columns().Amount, amount). Where(dao.V1JdCookieJdOrder.Columns().Category, category). 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()). OrderAsc(dao.V1JdCookieJdOrder.Columns().CreatedAt). Limit(1). diff --git a/internal/logic/jd_cookie/rotation.go b/internal/logic/jd_cookie/rotation.go index 33464b46..40541efc 100644 --- a/internal/logic/jd_cookie/rotation.go +++ b/internal/logic/jd_cookie/rotation.go @@ -251,18 +251,8 @@ func (s *sJdCookie) UpdateJdOrderStatus(ctx context.Context, jdOrderId string, s changeType = consts.JdOrderChangeTypeSend } - // 获取当前关联的订单ID - 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 - } - } + // 获取关联的订单ID + orderId := oldOrder.OrderId payUrl := wxPayUrl if payUrl == "" { diff --git a/internal/model/do/v_1_jd_cookie_jd_order.go b/internal/model/do/v_1_jd_cookie_jd_order.go index d620968c..fe4bb41b 100644 --- a/internal/model/do/v_1_jd_cookie_jd_order.go +++ b/internal/model/do/v_1_jd_cookie_jd_order.go @@ -23,7 +23,7 @@ type V1JdCookieJdOrder struct { WxPayUrl any // 微信支付链接 WxPayExpireAt *gtime.Time // 微信支付链接过期时间 OrderExpireAt *gtime.Time // 订单过期时间(默认24小时) - CurrentOrderId any // 当前关联的订单ID + OrderId any // 关联的用户订单号 PaidAt *gtime.Time // 支付完成时间 CardNo any // 卡号 CardPassword any // 卡密 diff --git a/internal/model/entity/v_1_jd_cookie_jd_order.go b/internal/model/entity/v_1_jd_cookie_jd_order.go index bb991a8c..a89ab74d 100644 --- a/internal/model/entity/v_1_jd_cookie_jd_order.go +++ b/internal/model/entity/v_1_jd_cookie_jd_order.go @@ -22,7 +22,7 @@ type V1JdCookieJdOrder struct { WxPayUrl string `json:"wxPayUrl" orm:"wx_pay_url" description:"微信支付链接"` WxPayExpireAt *gtime.Time `json:"wxPayExpireAt" orm:"wx_pay_expire_at" description:"微信支付链接过期时间"` 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:"支付完成时间"` CardNo string `json:"cardNo" orm:"card_no" description:"卡号"` CardPassword string `json:"cardPassword" orm:"card_password" description:"卡密"` diff --git a/internal/model/jd_cookie.go b/internal/model/jd_cookie.go index fd745b0f..56a64827 100644 --- a/internal/model/jd_cookie.go +++ b/internal/model/jd_cookie.go @@ -6,6 +6,13 @@ import "kami/internal/consts" // JD Cookie 相关模型结构体 // ==================================================================================== +// CreateOrderReq 创建订单请求参数 +type CreateOrderReq struct { + UserOrderId string `json:"userOrderId" dc:"用户订单号"` + Amount float64 `json:"amount" dc:"订单金额"` + Category consts.RedeemOrderCardCategory `json:"category" dc:"卡券类别"` +} + // CreateOrderResult 创建订单返回结果 type CreateOrderResult struct { WxPayUrl string `json:"wxPayUrl" dc:"微信支付链接"` diff --git a/internal/service/jd_cookie.go b/internal/service/jd_cookie.go index 07d6448f..a15fd3e9 100644 --- a/internal/service/jd_cookie.go +++ b/internal/service/jd_cookie.go @@ -10,7 +10,6 @@ import ( v1 "kami/api/jd_cookie/v1" "kami/internal/consts" "kami/internal/model" - "kami/internal/model/entity" ) type ( @@ -38,7 +37,7 @@ type ( // GetJdOrderHistoryByJdOrderId 根据京东订单ID获取京东订单历史 GetJdOrderHistoryByJdOrderId(ctx context.Context, jdOrderId string, page int, size int) (list []*v1.JdOrderHistoryInfo, total int, err error) // 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(ctx context.Context, jdOrderId string) (order *v1.JdOrderInfo, err error) // ListJdOrder 京东订单列表查询 @@ -50,9 +49,7 @@ type ( // ExtractCardInfo 提取卡密信息 ExtractCardInfo(ctx context.Context, jdOrderId string) error // Callback TODO:临时的回调 - Callback(ctx context.Context, orderId string, userOrderId string, amount float64) - // ShouldExtractCard 判断是否需要提取卡密 - ShouldExtractCard(ctx context.Context, jdOrder *entity.V1JdCookieJdOrder) bool + Callback(ctx context.Context, orderId string) // CleanupExpiredOrders 清理过期订单(定时任务) CleanupExpiredOrders(ctx context.Context) error // ReleaseExpiredJdOrders 释放过期京东订单的关联(使其可以被复用) diff --git a/openspec/AGENTS.md b/openspec/AGENTS.md new file mode 100644 index 00000000..e616c80c --- /dev/null +++ b/openspec/AGENTS.md @@ -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//`. +3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement. +4. Run `openspec validate --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 --type spec` (use `--json` for filters) + - Change: `openspec show --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//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//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. diff --git a/openspec/project.md b/openspec/project.md new file mode 100644 index 00000000..1da7c23b --- /dev/null +++ b/openspec/project.md @@ -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